import React, { useEffect, useState } from 'react' import PropTypes from 'prop-types' import Link from '@material-ui/core/Link' import Dialog from '@material-ui/core/Dialog' import IconButton from '@material-ui/core/IconButton' import TableContainer from '@material-ui/core/TableContainer' import Table from '@material-ui/core/Table' import TableBody from '@material-ui/core/TableBody' import TableRow from '@material-ui/core/TableRow' import TableCell from '@material-ui/core/TableCell' import Paper from '@material-ui/core/Paper' import CloudDownloadIcon from '@material-ui/icons/CloudDownload' import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder' import FileCopyIcon from '@material-ui/icons/FileCopy' import Button from '@material-ui/core/Button' import { humanize, underscore } from 'inflection' import { useGetOne, usePermissions, useTranslate, useNotify } from 'react-admin' import { Tabs, Tab } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import config from '../config' import { DialogTitle } from './DialogTitle' import { DialogContent } from './DialogContent' import { INSIGHTS_DOC_URL } from '../consts.js' import subsonic from '../subsonic/index.js' import { Typography } from '@material-ui/core' import TableHead from '@material-ui/core/TableHead' import { configToToml, separateAndSortConfigs } from './aboutUtils' const useStyles = makeStyles((theme) => ({ configNameColumn: { maxWidth: '200px', width: '200px', wordWrap: 'break-word', overflowWrap: 'break-word', }, envVarColumn: { maxWidth: '250px', width: '250px', fontFamily: 'monospace', wordWrap: 'break-word', overflowWrap: 'break-word', }, copyButton: { marginBottom: theme.spacing(2), marginTop: theme.spacing(1), }, devSectionHeader: { '& td': { paddingTop: theme.spacing(2), paddingBottom: theme.spacing(2), borderTop: `2px solid ${theme.palette.divider}`, borderBottom: `1px solid ${theme.palette.divider}`, textAlign: 'left', fontWeight: 600, }, }, configContainer: { paddingTop: theme.spacing(1), }, tableContainer: { maxHeight: '60vh', overflow: 'auto', }, devFlagsTitle: { fontWeight: 600, }, expandableDialog: { transition: 'max-width 300ms ease', }, })) const links = { homepage: 'navidrome.org', reddit: 'reddit.com/r/Navidrome', twitter: 'twitter.com/navidrome', discord: 'discord.gg/xh7j7yF', source: 'github.com/navidrome/navidrome', bugReports: 'github.com/navidrome/navidrome/issues/new/choose', featureRequests: 'github.com/navidrome/navidrome/discussions/new', } const LinkToVersion = ({ version }) => { if (version === 'dev') { return <>{version} } const parts = version.split(' ') const commitID = parts[1].replace(/[()]/g, '') const isSnapshot = version.includes('SNAPSHOT') const url = isSnapshot ? `https://github.com/navidrome/navidrome/compare/v${ parts[0].split('-')[0] }...${commitID}` : `https://github.com/navidrome/navidrome/releases/tag/v${parts[0]}` return ( <> {parts[0]} {' (' + commitID + ')'} ) } const ShowVersion = ({ uiVersion, serverVersion }) => { const translate = useTranslate() const showRefresh = uiVersion !== serverVersion return ( <> {translate('menu.version')}: {showRefresh && ( UI {translate('menu.version')}:
window.location.reload()}> {translate('ra.notification.new_version')}
)} ) } const AboutTabContent = ({ uiVersion, serverVersion, insightsData, loading, permissions, }) => { const translate = useTranslate() const lastRun = !loading && insightsData?.lastRun let insightsStatus = 'N/A' if (lastRun === 'disabled') { insightsStatus = translate('about.links.insights.disabled') } else if (lastRun && lastRun?.startsWith('1969-12-31')) { insightsStatus = translate('about.links.insights.waiting') } else if (lastRun) { insightsStatus = lastRun } return ( {Object.keys(links).map((key) => { return ( {translate(`about.links.${key}`, { _: humanize(underscore(key)), })} : {links[key]} ) })} {permissions === 'admin' ? ( {translate(`about.links.lastInsightsCollection`)}: {insightsStatus} ) : null} ko-fi.com/deluan
) } const ConfigTabContent = ({ configData }) => { const classes = useStyles() const translate = useTranslate() const notify = useNotify() if (!configData || !configData.config) { return null } // Use the shared separation and sorting logic const { regularConfigs, devConfigs } = separateAndSortConfigs( configData.config, ) const handleCopyToml = async () => { try { const tomlContent = configToToml(configData, translate) await navigator.clipboard.writeText(tomlContent) notify(translate('about.config.exportSuccess'), 'info') } catch (err) { // eslint-disable-next-line no-console console.error('Failed to copy TOML:', err) notify(translate('about.config.exportFailed'), 'error') } } const handleDownloadToml = () => { const tomlContent = configToToml(configData, translate) const tomlFile = new File([tomlContent], 'navidrome.toml', { type: 'text/plain', }) const tomlFileLink = document.createElement('a') const tomlFileUrl = URL.createObjectURL(tomlFile) tomlFileLink.href = tomlFileUrl tomlFileLink.download = tomlFile.name tomlFileLink.click() URL.revokeObjectURL(tomlFileUrl) } return (
{translate('about.config.configName')} {translate('about.config.environmentVariable')} {translate('about.config.currentValue')} {configData?.configFile && ( {translate('about.config.configurationFile')} ND_CONFIGFILE {configData.configFile} )} {regularConfigs.map(({ key, envVar, value }) => ( {key} {envVar} {String(value)} ))} {devConfigs.length > 0 && ( 🚧 {translate('about.config.devFlagsHeader')} )} {devConfigs.map(({ key, envVar, value }) => ( {key} {envVar} {String(value)} ))}
) } const TabContent = ({ tab, setTab, showConfigTab, uiVersion, serverVersion, insightsData, loading, permissions, configData, }) => { const translate = useTranslate() return ( {showConfigTab && ( setTab(value)}> )} {showConfigTab && ( )} ) } const AboutDialog = ({ open, onClose }) => { const classes = useStyles() const { permissions } = usePermissions() const { data: insightsData, loading } = useGetOne( 'insights', 'insights_status', ) const [serverVersion, setServerVersion] = useState('') const showConfigTab = permissions === 'admin' && config.devUIShowConfig const [tab, setTab] = useState(0) const { data: configData } = useGetOne('config', 'config', { enabled: showConfigTab, }) const expanded = showConfigTab && tab === 1 const uiVersion = config.version useEffect(() => { subsonic .ping() .then((resp) => resp.json['subsonic-response']) .then((data) => { if (data.status === 'ok') { setServerVersion(data.serverVersion) } }) .catch((e) => { // eslint-disable-next-line no-console console.error('error pinging server', e) }) }, [setServerVersion]) return ( Navidrome Music Server ) } AboutDialog.propTypes = { open: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, } export { AboutDialog, LinkToVersion }