diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json
index c40e66866..4fd1bc371 100644
--- a/ui/src/i18n/en.json
+++ b/ui/src/i18n/en.json
@@ -271,6 +271,13 @@
"path": "Path",
"remotePath": "Remote Path",
"lastScanAt": "Last Scan",
+ "songCount": "Songs",
+ "albumCount": "Albums",
+ "artistCount": "Artists",
+ "scanCount": "Scan Count",
+ "missingFileCount": "Missing Files",
+ "size": "Size",
+ "duration": "Duration",
"totalSongs": "Songs",
"totalAlbums": "Albums",
"totalArtists": "Artists",
@@ -282,6 +289,11 @@
"createdAt": "Created",
"updatedAt": "Updated"
},
+ "sections": {
+ "basic": "Basic Information",
+ "statistics": "Statistics",
+ "scan": "Scan Information"
+ },
"actions": {
"scan": "Scan Library",
"manageUsers": "Manage User Access",
diff --git a/ui/src/library/LibraryEdit.jsx b/ui/src/library/LibraryEdit.jsx
index da46a73ab..75c68863a 100644
--- a/ui/src/library/LibraryEdit.jsx
+++ b/ui/src/library/LibraryEdit.jsx
@@ -1,89 +1,48 @@
import React, { useCallback } from 'react'
import {
Edit,
- SimpleForm,
+ FormWithRedirect,
TextInput,
required,
- Toolbar,
SaveButton,
- NumberField,
DateField,
- FunctionField,
useTranslate,
useMutation,
useNotify,
useRedirect,
+ NumberInput,
} from 'react-admin'
-import { makeStyles } from '@material-ui/core/styles'
-import { Divider, Typography } from '@material-ui/core'
+import { Typography, Box, Toolbar } from '@material-ui/core'
import DeleteLibraryButton from './DeleteLibraryButton'
import { Title } from '../common'
-import { formatBytes } from '../utils/index.js'
-
-const useStyles = makeStyles({
- toolbar: {
- display: 'flex',
- justifyContent: 'space-between',
- },
- divider: {
- marginTop: '1em',
- marginBottom: '1em',
- },
- stats: {
- display: 'grid',
- gridTemplateColumns: '1fr 1fr 1fr',
- gap: '0 1em',
- '& > *': {
- padding: '0.2em 0',
- },
- },
-})
+import { formatBytes, formatDuration2, formatNumber } from '../utils/index.js'
const LibraryTitle = ({ record }) => {
const translate = useTranslate()
const resourceName = translate('resources.library.name', { smart_count: 1 })
return (
-
+
)
}
-const LibraryToolbar = (props) => (
-
-
- {props.record && props.record.id !== 1 && (
-
- )}
+const CustomToolbar = (props) => (
+
+
+
+
+
)
-const formatDuration = (totalSeconds) => {
- if (totalSeconds == null || totalSeconds < 0) {
- return '0s'
- }
- const hours = Math.floor(totalSeconds / 3600)
- const minutes = Math.floor((totalSeconds % 3600) / 60)
- const seconds = Math.floor(totalSeconds % 60)
-
- const parts = []
- if (hours > 0) {
- parts.push(`${hours}h`)
- }
- if (minutes > 0) {
- parts.push(`${minutes}m`)
- }
- if (seconds > 0 || parts.length === 0) {
- parts.push(`${seconds}s`)
- }
- return parts.join(' ')
-}
-
const LibraryEdit = (props) => {
- const classes = useStyles()
const translate = useTranslate()
const [mutate] = useMutation()
const notify = useNotify()
const redirect = useRedirect()
- const isFirstLibrary = props.id === '1'
const save = useCallback(
async (values) => {
@@ -92,7 +51,7 @@ const LibraryEdit = (props) => {
{
type: 'update',
resource: 'library',
- payload: { id: props.id, data: values },
+ payload: { id: values.id, data: values },
},
{ returnPromise: true },
)
@@ -106,99 +65,153 @@ const LibraryEdit = (props) => {
}
}
},
- [mutate, notify, redirect, props.id],
+ [mutate, notify, redirect],
)
return (
} undoable={false} {...props}>
- } save={save}>
-
-
-
-
- Statistics
-
- {/*eslint-disable-next-line react/no-unknown-property*/}
-
-
-
- {translate('resources.library.fields.totalSongs')}
-
-
-
-
-
- {translate('resources.library.fields.totalAlbums')}
-
-
-
-
-
- {translate('resources.library.fields.totalArtists')}
-
-
-
-
-
- {translate('resources.library.fields.totalFolders')}
-
-
-
-
-
- {translate('resources.library.fields.totalFiles')}
-
-
-
-
-
- {translate('resources.library.fields.totalMissingFiles')}
-
-
-
-
-
- {translate('resources.library.fields.totalSize')}
-
- formatBytes(record.totalSize)} />
-
-
-
- {translate('resources.library.fields.totalDuration')}
-
- formatDuration(record.totalDuration)}
+ (
+
-
-
- {/*eslint-disable-next-line react/no-unknown-property*/}
-
-
-
- {translate('resources.library.fields.lastScanAt')}
-
-
-
-
-
- {translate('resources.library.fields.createdAt')}
-
-
-
-
-
- {translate('resources.library.fields.updatedAt')}
-
-
-
-
-
+
+ )}
+ />
)
}
diff --git a/ui/src/utils/formatters.js b/ui/src/utils/formatters.js
index ae27f230f..8141e2366 100644
--- a/ui/src/utils/formatters.js
+++ b/ui/src/utils/formatters.js
@@ -25,6 +25,42 @@ export const formatDuration = (d) => {
return `${days > 0 ? days + ':' : ''}${f}`
}
+export const formatDuration2 = (totalSeconds) => {
+ if (totalSeconds == null || totalSeconds < 0) {
+ return '0s'
+ }
+ const days = Math.floor(totalSeconds / 86400)
+ const hours = Math.floor((totalSeconds % 86400) / 3600)
+ const minutes = Math.floor((totalSeconds % 3600) / 60)
+ const seconds = Math.floor(totalSeconds % 60)
+
+ const parts = []
+
+ if (days > 0) {
+ // When days are present, show only d h m (3 levels max)
+ parts.push(`${days}d`)
+ if (hours > 0) {
+ parts.push(`${hours}h`)
+ }
+ if (minutes > 0) {
+ parts.push(`${minutes}m`)
+ }
+ } else {
+ // When no days, show h m s (3 levels max)
+ if (hours > 0) {
+ parts.push(`${hours}h`)
+ }
+ if (minutes > 0) {
+ parts.push(`${minutes}m`)
+ }
+ if (seconds > 0 || parts.length === 0) {
+ parts.push(`${seconds}s`)
+ }
+ }
+
+ return parts.join(' ')
+}
+
export const formatShortDuration = (ns) => {
// Convert nanoseconds to seconds
const seconds = ns / 1e9
@@ -58,3 +94,8 @@ export const formatFullDate = (date, locale) => {
}
return new Date(date).toLocaleDateString(locale, options)
}
+
+export const formatNumber = (value) => {
+ if (value === null || value === undefined) return '0'
+ return value.toLocaleString()
+}
diff --git a/ui/src/utils/formatters.test.js b/ui/src/utils/formatters.test.js
index 87b40f16b..922e775f3 100644
--- a/ui/src/utils/formatters.test.js
+++ b/ui/src/utils/formatters.test.js
@@ -1,7 +1,9 @@
import {
formatBytes,
formatDuration,
+ formatDuration2,
formatFullDate,
+ formatNumber,
formatShortDuration,
} from './formatters'
@@ -64,6 +66,85 @@ describe('formatShortDuration', () => {
})
})
+describe('formatDuration2', () => {
+ it('handles null and undefined values', () => {
+ expect(formatDuration2(null)).toEqual('0s')
+ expect(formatDuration2(undefined)).toEqual('0s')
+ })
+
+ it('handles negative values', () => {
+ expect(formatDuration2(-10)).toEqual('0s')
+ expect(formatDuration2(-1)).toEqual('0s')
+ })
+
+ it('formats zero seconds', () => {
+ expect(formatDuration2(0)).toEqual('0s')
+ })
+
+ it('formats seconds only', () => {
+ expect(formatDuration2(1)).toEqual('1s')
+ expect(formatDuration2(30)).toEqual('30s')
+ expect(formatDuration2(59)).toEqual('59s')
+ })
+
+ it('formats minutes and seconds', () => {
+ expect(formatDuration2(60)).toEqual('1m')
+ expect(formatDuration2(90)).toEqual('1m 30s')
+ expect(formatDuration2(119)).toEqual('1m 59s')
+ expect(formatDuration2(120)).toEqual('2m')
+ })
+
+ it('formats hours, minutes and seconds', () => {
+ expect(formatDuration2(3600)).toEqual('1h')
+ expect(formatDuration2(3661)).toEqual('1h 1m 1s')
+ expect(formatDuration2(7200)).toEqual('2h')
+ expect(formatDuration2(7260)).toEqual('2h 1m')
+ expect(formatDuration2(7261)).toEqual('2h 1m 1s')
+ })
+
+ it('handles decimal values by flooring', () => {
+ expect(formatDuration2(59.9)).toEqual('59s')
+ expect(formatDuration2(60.1)).toEqual('1m')
+ expect(formatDuration2(3600.9)).toEqual('1h')
+ })
+
+ it('formats days with maximum 3 levels (d h m)', () => {
+ expect(formatDuration2(86400)).toEqual('1d')
+ expect(formatDuration2(86461)).toEqual('1d 1m') // seconds dropped when days present
+ expect(formatDuration2(90061)).toEqual('1d 1h 1m') // seconds dropped when days present
+ expect(formatDuration2(172800)).toEqual('2d')
+ expect(formatDuration2(176400)).toEqual('2d 1h')
+ expect(formatDuration2(176460)).toEqual('2d 1h 1m')
+ expect(formatDuration2(176461)).toEqual('2d 1h 1m') // seconds dropped when days present
+ })
+})
+
+describe('formatNumber', () => {
+ it('handles null and undefined values', () => {
+ expect(formatNumber(null)).toEqual('0')
+ expect(formatNumber(undefined)).toEqual('0')
+ })
+
+ it('formats integers', () => {
+ expect(formatNumber(0)).toEqual('0')
+ expect(formatNumber(1)).toEqual('1')
+ expect(formatNumber(123)).toEqual('123')
+ expect(formatNumber(1000)).toEqual('1,000')
+ expect(formatNumber(1234567)).toEqual('1,234,567')
+ })
+
+ it('formats decimal numbers', () => {
+ expect(formatNumber(123.45)).toEqual('123.45')
+ expect(formatNumber(1234.567)).toEqual('1,234.567')
+ })
+
+ it('formats negative numbers', () => {
+ expect(formatNumber(-123)).toEqual('-123')
+ expect(formatNumber(-1234)).toEqual('-1,234')
+ expect(formatNumber(-123.45)).toEqual('-123.45')
+ })
+})
+
describe('formatFullDate', () => {
it('format dates', () => {
expect(formatFullDate('2011', 'en-US')).toEqual('2011')