From 22fbf40b4731c894b1cc4fbd86580806eb94e975 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Thu, 20 Aug 2026 15:07:56 +0000 Subject: [PATCH] feat(ui): Optimize musicbrainz/external links layout in play data --- package-lock.json | 321 ++++++++++++++++++ package.json | 1 + src/client/components/ActivityDetail.tsx | 2 +- src/client/components/ArtistCreditDisplay.tsx | 28 +- src/client/components/PlayData.tsx | 147 +++++--- src/client/components/icons/ChakraIcons.tsx | 47 ++- .../components/icons/custom/listenbrainz.svg | 1 + .../components/icons/custom/musicbrainz.svg | 1 + .../msComponent/MSComponentDetailed.tsx | 2 +- .../components/musicServices/Musicbrainz.tsx | 44 +++ src/client/vite-env.d.ts | 1 + vite.config.ts | 2 + 12 files changed, 529 insertions(+), 68 deletions(-) create mode 100644 src/client/components/icons/custom/listenbrainz.svg create mode 100644 src/client/components/icons/custom/musicbrainz.svg create mode 100644 src/client/components/musicServices/Musicbrainz.tsx diff --git a/package-lock.json b/package-lock.json index 5187439c..72584605 100644 --- a/package-lock.json +++ b/package-lock.json @@ -210,6 +210,7 @@ "typescript-eslint": "^8.59.3", "usehooks-ts": "^3.1.1", "vite": "^8.0.12", + "vite-plugin-svgr": "^5.2.0", "with-local-tmp-dir": "^7.0.1", "zocker": "^3.0.0", "zod": "^4.4.3" @@ -5829,6 +5830,258 @@ "node": ">=8" } }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, "node_modules/@svrooij/sonos": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@svrooij/sonos/-/sonos-2.5.0.tgz", @@ -10333,6 +10586,17 @@ "csstype": "^3.0.2" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dotenv": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", @@ -15116,6 +15380,16 @@ "get-func-name": "^2.0.1" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -16012,6 +16286,17 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -18572,6 +18857,17 @@ "node": ">=6" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -19037,6 +19333,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-parser": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.5.tgz", + "integrity": "sha512-FuT74puvVMJQ8sgFxgOKPex5MbxPeltqCXJV3wR9Xq+vPHaF+tTdE1lhJcwspUxjtjvXy1NwqcSLZx9UNwHawg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -20190,6 +20496,21 @@ "picocolors": "^1.1.1" } }, + "node_modules/vite-plugin-svgr": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-5.2.0.tgz", + "integrity": "sha512-qj2eAKF8C6PZWemVTvQA0xgQIcP1hHU6Buh7fl6BhvayWwnuxE+z417miKxeDvRWbDrupQ1oK99hfxElopJ3sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.3.0", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": ">=3.0.0" + } + }, "node_modules/vlc-client": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/vlc-client/-/vlc-client-1.1.1.tgz", diff --git a/package.json b/package.json index 0ff35490..3532fe4b 100644 --- a/package.json +++ b/package.json @@ -250,6 +250,7 @@ "typescript-eslint": "^8.59.3", "usehooks-ts": "^3.1.1", "vite": "^8.0.12", + "vite-plugin-svgr": "^5.2.0", "with-local-tmp-dir": "^7.0.1", "zocker": "^3.0.0", "zod": "^4.4.3" diff --git a/src/client/components/ActivityDetail.tsx b/src/client/components/ActivityDetail.tsx index 0305ed6f..44c9f325 100644 --- a/src/client/components/ActivityDetail.tsx +++ b/src/client/components/ActivityDetail.tsx @@ -255,7 +255,7 @@ export const ActivityDetails = (props: ActivityDetailProps) => { - + diff --git a/src/client/components/ArtistCreditDisplay.tsx b/src/client/components/ArtistCreditDisplay.tsx index 77fe1800..3532529c 100644 --- a/src/client/components/ArtistCreditDisplay.tsx +++ b/src/client/components/ArtistCreditDisplay.tsx @@ -1,18 +1,18 @@ import { Fragment } from 'react'; -import type {ArtistCredit as AC} from '../../core/Atomic'; +import type { ArtistCredit as AC } from '../../core/Atomic'; import { HStack, Tag } from "@chakra-ui/react"; -import { SiMusicbrainz } from "react-icons/si"; -import { Tooltip } from './ChakraTooltip'; +import { MusicbrainzInfoIcon } from './musicServices/Musicbrainz'; -export const ArtistCredit = (props: { data: AC, showIdLink?: boolean }) => { +export const ArtistCredit = (props: { data: AC, showLinks?: boolean, showMbid?: boolean }) => { const { data, - showIdLink = true + showLinks = true, + showMbid } = props; - if (!showIdLink || Object.keys(data).length === 1) { + if (!showLinks) { return data.name; } @@ -20,19 +20,19 @@ export const ArtistCredit = (props: { data: AC, showIdLink?: boolean }) => { {data.name} - {data.mbid !== undefined ? : null} + {data.mbid !== undefined ? : null} } -export const ArtistCreditTags = (props: { data: AC[], showIdLink?: boolean }) => { - return {props.data.map((x, index) => { - return ( +export const ArtistCreditTags = (props: { data: AC[], showLinks?: boolean, showMbid?: boolean }) => ( + + {props.data.map((x, index) => ( - + - ); - })} -} \ No newline at end of file + ))} + +) \ No newline at end of file diff --git a/src/client/components/PlayData.tsx b/src/client/components/PlayData.tsx index 77999cdf..e0ace343 100644 --- a/src/client/components/PlayData.tsx +++ b/src/client/components/PlayData.tsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; -import { EmptyState, DataList, HStack, Tabs, Box, Flex, Stack, Text, Separator, IconButton, Container, Float, Icon, Link, Span, Show } from "@chakra-ui/react" +import React, { useCallback, useState, type ComponentProps } from 'react'; +import { EmptyState, DataList, HStack, Tabs, Box, Flex, Stack, Text, Separator, IconButton, Container, Float, Icon, Link, Span, Show, Menu, Group, Portal, type MenuItemProps, type MenuSelectionDetails } from "@chakra-ui/react" import { LuCode, LuText, LuCheck, LuX } from "react-icons/lu" -import type {JsonPlayObject, PlayObjectMinimal} from '../../core/Atomic.js'; +import type { JsonPlayObject, PlayObjectMinimal } from '../../core/Atomic.js'; import { shortTodayAwareFormat, timeToHumanTimestamp } from '../../core/TimeUtils.js'; import dayjs from 'dayjs'; import { ChakraCodeBlock } from './CodeBlock.js'; @@ -10,6 +10,10 @@ import { formatNumber } from '../../core/DataUtils.js'; import { Muted } from './Typography.js'; import { ArtistCreditTags } from './ArtistCreditDisplay.js'; import { MSErrorBoundary } from './ErrorBoundary.js'; +import { EllipsisButton, EyeClosedIcon, EyeIcon, getMusicServiceIconElement } from './icons/ChakraIcons.js'; +import { MusicbrainzInfoIcon } from './musicServices/Musicbrainz.js'; +import type { IconType } from 'react-icons/lib'; +import { capitalize } from '../../core/StringUtils.js'; const EmptyPlayData = () => { return ( @@ -35,6 +39,17 @@ export interface PlayInfoProps { dates?: false | 'all' | 'played' | 'seen' } +const primaryActionProps: ComponentProps = { + //margin: "1px", + variant: "outline", + size: 'xs' +} + +const menuItem = (Icon: IconType, value: string, name?: string) => (props: Pick = {}) => ({name ?? capitalize(value)}) + +const MenuMbidShow = menuItem(EyeIcon, 'mbidShow', 'Show MBIDs'); +const MenuMbidHide = menuItem(EyeClosedIcon, 'mbidHide', 'Hide MBIDs'); + export const PlayData = (props?: PlayInfoProps) => { const { play, @@ -47,56 +62,90 @@ export const PlayData = (props?: PlayInfoProps) => { const [codeMode, setCodeMode] = useState(false); + const [showMbid, setShowMBid] = useState(false); - let code: React.JSX.Element | null = null; - - const comparable = showCompare && final !== undefined; + const menuCb = useCallback((select: MenuSelectionDetails) => { + if (select.value === 'mbidShow') { + setShowMBid(true); + } else if (select.value === 'mbidHide') { + setShowMBid(false); + } + }, [setShowMBid]) if (play === undefined) { return } + let code: React.JSX.Element | null = null; + + const comparable = showCompare && final !== undefined; + + const menuItems: React.JSX.Element[] = [ + showMbid ? : + ]; if (showCodeToggle) { code = ( - setCodeMode(!codeMode)}> + setCodeMode(!codeMode)}> {codeMode ? : } ); } + let content: React.JSX.Element; + if (!comparable) { - return ( - {code} - {codeMode ? : } - ); + content = codeMode ? : ; + } else { + content = ( + + + Initial + Final + + + {codeMode ? : } + + + {codeMode ? : } + + + ) } return ( - {code} - - - Initial - Final - - - {codeMode ? : } - - - {codeMode ? : } - - + + + + + {code} + + + + + + + + {menuItems} + + + + + + + {content} ); } -export const PlayDataDataList = (props: { play: JsonPlayObject, dates: DisplayDates }) => { +export const PlayDataDataList = (props: { play: JsonPlayObject, dates: DisplayDates, showMbid?: boolean }) => { const { play, - dates + dates, + showMbid = false } = props; @@ -128,16 +177,23 @@ export const PlayDataDataList = (props: { play: JsonPlayObject, dates: DisplayDa url: { web: webUrl, origin: originUrl - } = {} + } = {}, + musicService } = {} } = play; - let titleElm: React.JSX.Element; - if(webUrl !== undefined || originUrl !== undefined) { - titleElm = {track} - } else { - titleElm = {track}; + const titleLinks: React.JSX.Element[] = []; + if (webUrl !== undefined || originUrl !== undefined) { + titleLinks.push({getMusicServiceIconElement(musicService)}); } + if (brainz.track !== undefined) { + titleLinks.push() + } + if (brainz.recording !== undefined) { + titleLinks.push() + } + + const titleElm = {track}{titleLinks} return ( @@ -150,13 +206,20 @@ export const PlayDataDataList = (props: { play: JsonPlayObject, dates: DisplayDa Artists {artists.length === 0 ? (No Artists) : - } + } {albumArtistElm} Album - {play.data.album} + + + {play.data.album} + + + + + @@ -172,23 +235,9 @@ export const PlayDataDataList = (props: { play: JsonPlayObject, dates: DisplayDa Repeat? - {repeat ? : } + {repeat ? : } - - 0}> - - MBIDs - - - Track: {brainz.track} - Recording: {brainz.recording} - Album: {brainz.album} - - - - - ) } diff --git a/src/client/components/icons/ChakraIcons.tsx b/src/client/components/icons/ChakraIcons.tsx index 951854bd..2a0438ab 100644 --- a/src/client/components/icons/ChakraIcons.tsx +++ b/src/client/components/icons/ChakraIcons.tsx @@ -32,11 +32,22 @@ import { VscDebugRestart } from 'react-icons/vsc'; import { MdOutlineFiberNew } from "react-icons/md"; import { RiZzzFill } from "react-icons/ri"; import { SiGoogledocs } from "react-icons/si"; -import type { Clipboard} from "@chakra-ui/react"; +import type { Clipboard, IconProps} from "@chakra-ui/react"; import { IconButton, useClipboard, Spinner, Icon } from "@chakra-ui/react" -import type {ComponentProps, PropsWithChildren} from 'react'; +import type {ComponentProps, PropsWithChildren, ReactNode} from 'react'; import type {IconBaseProps, IconType} from "react-icons/lib"; +// below are from https://selfh.st/icons/ +import LZ from "./custom/listenbrainz.svg?react"; +import Musicbrainz from "./custom/musicbrainz.svg?react"; + +import { + SiSpotify, + SiYoutube, + SiJellyfin, + SiPlex + } from "react-icons/si"; + export const makeIconButton = (Icon: IconType) => (props: PropsWithChildren> & { iconProps?: IconBaseProps, loading?: boolean }) => { const { iconProps = {}, @@ -202,4 +213,34 @@ export const SparkleIcon = makeChakraIcon(LuSparkles); export const UnlockIconRaw = LuLockOpen; export const UnlockIcon = makeChakraIcon(LuLockOpen); -export const UnlockButton = makeIconButton(LuLockOpen); \ No newline at end of file +export const UnlockButton = makeIconButton(LuLockOpen); + +export const getMusicServiceIcon = (service: string): IconType => { + const lower = service.toLocaleLowerCase(); + switch(lower) { + case 'spotify': + return SiSpotify; + case 'musicbrainz': + return Musicbrainz as unknown as IconType; + case 'youtube': + return SiYoutube; + case 'jellyfin': + return SiJellyfin; + case 'plex': + return SiPlex; + case 'listenbrainz': + return LZ as unknown as IconType; + default: + return LuExternalLink; + } +} + +export const getMusicServiceIconElement = (service: string): ReactNode => { + const ServiceIcon = getMusicServiceIcon(service); + return ; +} + +export const getMusicServiceChakraIcon = (service: string) => { + const ServiceIcon = getMusicServiceIcon(service); + return (props: IconProps = {}) => ; +} \ No newline at end of file diff --git a/src/client/components/icons/custom/listenbrainz.svg b/src/client/components/icons/custom/listenbrainz.svg new file mode 100644 index 00000000..c8025820 --- /dev/null +++ b/src/client/components/icons/custom/listenbrainz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/components/icons/custom/musicbrainz.svg b/src/client/components/icons/custom/musicbrainz.svg new file mode 100644 index 00000000..9ffd21d2 --- /dev/null +++ b/src/client/components/icons/custom/musicbrainz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/components/msComponent/MSComponentDetailed.tsx b/src/client/components/msComponent/MSComponentDetailed.tsx index 8af9783c..9d2abf1f 100644 --- a/src/client/components/msComponent/MSComponentDetailed.tsx +++ b/src/client/components/msComponent/MSComponentDetailed.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo, type ComponentProps, useEffect } from "rea import { Portal, Group, Span, Menu, Box, Heading, Skeleton, Wrap, HStack, Stack, Flex, Text, Card, Button, CloseButton, SkeletonText, type BadgeProps, type MenuItemProps, createOverlay, Dialog, type MenuSelectionDetails } from '@chakra-ui/react'; import { COMPONENT_STATE, type ComponentClientApiJson, type ComponentCommonApiJson, type ComponentsApiJson, type ComponentState, type ComponentStateBody, isComponentClientApiJson, isComponentSourceApiJson, type MsSseEvent, type MsSseEventPayload } from "../../../core/Api.js"; import { capitalize } from "../../../core/StringUtils.js"; -import { ChevronLeftButton, EllipsisButton, ExternalLinkIcon, EyeButton, EyeClosedIcon, EyeIcon, IdleIcon, PowerButton, PowerIcon, PowerOffButton, PowerOffIcon, RetryButton, RetryIcon, UnlockButton, UnlockIconRaw } from "../icons/ChakraIcons.js"; +import { ChevronLeftButton, EllipsisButton, ExternalLinkIcon, EyeButton, EyeClosedIcon, EyeIcon, IdleIcon, PowerButton, PowerIcon, type PowerOffButton, PowerOffIcon, RetryButton, RetryIcon, UnlockButton, UnlockIconRaw } from "../icons/ChakraIcons.js"; import { PlayersContainer, PlayersContainerFetchable } from "../chakraPlayer/Player.js"; import { Tooltip } from "../ToggleTip.js"; import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'; diff --git a/src/client/components/musicServices/Musicbrainz.tsx b/src/client/components/musicServices/Musicbrainz.tsx new file mode 100644 index 00000000..71a7cdfe --- /dev/null +++ b/src/client/components/musicServices/Musicbrainz.tsx @@ -0,0 +1,44 @@ +import { Icon, type IconProps, Separator, HStack } from "@chakra-ui/react"; +import { getMusicServiceIconElement } from "../icons/ChakraIcons"; +import { capitalize } from "../../../core/StringUtils"; +import { Tooltip } from "../ChakraTooltip"; +import type React from "react"; +import { Muted } from "../Typography"; + +export interface MusicbrainzInfoIconProps { + type: 'recording' | 'release' | 'track' | 'artist', + mbid: string, + link?: boolean + tooltip?: boolean + iconProps?: IconProps + showMbid?: boolean +} +export const MusicbrainzInfoIcon = (props: MusicbrainzInfoIconProps) => { + + const { + iconProps = {}, + link, + mbid, + tooltip, + type, + showMbid = false, + } = props; + + const icon = {getMusicServiceIconElement('musicbrainz')}; + + let content: React.JSX.Element; + if (link) { + content = {icon} + } else { + content = icon; + } + let visibleMbid: React.JSX.Element = null; + if(showMbid) { + visibleMbid = <>{`${capitalize(type)}`} {mbid} + } + + if (tooltip) { + return {visibleMbid}{content}; + } + return {visibleMbid}content; +} \ No newline at end of file diff --git a/src/client/vite-env.d.ts b/src/client/vite-env.d.ts index a6880ca6..a9896dea 100644 --- a/src/client/vite-env.d.ts +++ b/src/client/vite-env.d.ts @@ -1,2 +1,3 @@ /// +/// declare const __USE_HASH_ROUTER__: boolean diff --git a/vite.config.ts b/vite.config.ts index 76f9ace0..14a627f8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,7 @@ import babel from '@rolldown/plugin-babel'; import normalizeUrl from "normalize-url"; import { defineConfig } from 'vite'; import tailwindcss from '@tailwindcss/vite' +import svgr from "vite-plugin-svgr"; import { dirname, resolve } from 'node:path' const QUOTES_UNWRAP_REGEX: RegExp = new RegExp(/^"(.*)"$/); @@ -50,6 +51,7 @@ export default defineConfig(() => { presets: [reactCompilerPreset()] }), tailwindcss(), + svgr() ], build: { sourcemap: true, -- 2.51.2