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,