Compare commits

...

8 Commits

Author SHA1 Message Date
Deluan Quintão
03b1a5952a
Merge 5cb851f2a86231015faaffd12f1342bdeb7a3199 into e86dc03619ffb8477083de23bb4daed567ef0a2c 2025-11-02 12:05:17 -05:00
pca006132
e86dc03619
fix(ui): allow scrolling in play queue by adding delay (#4562) 2025-11-01 20:47:03 -04:00
Deluan Quintão
775626e037
refactor(scanner): optimize update artist's statistics using normalized media_file_artists table (#4641)
Optimized to use the normalized media_file_artists table instead of parsing JSONB

Signed-off-by: Deluan <deluan@navidrome.org>
2025-11-01 20:25:33 -04:00
Deluan Quintão
5cb851f2a8
Merge branch 'master' into fix/default-language-app-startup 2025-05-25 17:55:23 -04:00
Deluan
5c18951e31 fix(ui): streamline locale setting in App component
Signed-off-by: Deluan <deluan@navidrome.org>
2025-04-28 09:52:53 -04:00
Deluan
2b744c878e fix(ui): move default language initialization to Admin component
Signed-off-by: Deluan <deluan@navidrome.org>
2025-04-28 09:42:21 -04:00
Deluan
4548e75d49 style(ui): format App.jsx with Prettier
Ran Prettier on ui/src/App.jsx to satisfy code style checks after adding default-language useEffect.
2025-04-25 08:12:44 -04:00
Deluan
841af03393 fix: load ND_DEFAULTLANGUAGE on app startup
Added  in  to apply  on initial mount, ensuring the locale is set even when the login page is skipped by reverse-proxy authentication. Removed the redundant language-init effect from . Fixes #3605.
2025-04-25 08:09:06 -04:00
4 changed files with 40 additions and 44 deletions

View File

@ -400,23 +400,16 @@ func (r *artistRepository) RefreshStats(allArtists bool) (int64, error) {
// This now calculates per-library statistics and stores them in library_artist.stats // This now calculates per-library statistics and stores them in library_artist.stats
batchUpdateStatsSQL := ` batchUpdateStatsSQL := `
WITH artist_role_counters AS ( WITH artist_role_counters AS (
SELECT jt.atom AS artist_id, SELECT mfa.artist_id,
mf.library_id, mf.library_id,
substr( mfa.role,
replace(jt.path, '$.', ''),
1,
CASE WHEN instr(replace(jt.path, '$.', ''), '[') > 0
THEN instr(replace(jt.path, '$.', ''), '[') - 1
ELSE length(replace(jt.path, '$.', ''))
END
) AS role,
count(DISTINCT mf.album_id) AS album_count, count(DISTINCT mf.album_id) AS album_count,
count(mf.id) AS count, count(DISTINCT mf.id) AS count,
sum(mf.size) AS size sum(mf.size) AS size
FROM media_file mf FROM media_file_artists mfa
JOIN json_tree(mf.participants) jt ON jt.key = 'id' AND jt.atom IS NOT NULL JOIN media_file mf ON mfa.media_file_id = mf.id
WHERE jt.atom IN (ROLE_IDS_PLACEHOLDER) -- Will replace with actual placeholders WHERE mfa.artist_id IN (ROLE_IDS_PLACEHOLDER) -- Will replace with actual placeholders
GROUP BY jt.atom, mf.library_id, role GROUP BY mfa.artist_id, mf.library_id, mfa.role
), ),
artist_total_counters AS ( artist_total_counters AS (
SELECT mfa.artist_id, SELECT mfa.artist_id,
@ -445,24 +438,24 @@ func (r *artistRepository) RefreshStats(allArtists bool) (int64, error) {
), ),
combined_counters AS ( combined_counters AS (
SELECT artist_id, library_id, role, album_count, count, size FROM artist_role_counters SELECT artist_id, library_id, role, album_count, count, size FROM artist_role_counters
UNION UNION ALL
SELECT artist_id, library_id, role, album_count, count, size FROM artist_total_counters SELECT artist_id, library_id, role, album_count, count, size FROM artist_total_counters
UNION UNION ALL
SELECT artist_id, library_id, role, album_count, count, size FROM artist_participant_counter SELECT artist_id, library_id, role, album_count, count, size FROM artist_participant_counter
), ),
library_artist_counters AS ( library_artist_counters AS (
SELECT artist_id, SELECT artist_id,
library_id, library_id,
json_group_object( json_group_object(
replace(role, '"', ''), role,
json_object('a', album_count, 'm', count, 's', size) json_object('a', album_count, 'm', count, 's', size)
) AS counters ) AS counters
FROM combined_counters FROM combined_counters
GROUP BY artist_id, library_id GROUP BY artist_id, library_id
) )
UPDATE library_artist UPDATE library_artist
SET stats = coalesce((SELECT counters FROM library_artist_counters lac SET stats = coalesce((SELECT counters FROM library_artist_counters lac
WHERE lac.artist_id = library_artist.artist_id WHERE lac.artist_id = library_artist.artist_id
AND lac.library_id = library_artist.library_id), '{}') AND lac.library_id = library_artist.library_id), '{}')
WHERE library_artist.artist_id IN (ROLE_IDS_PLACEHOLDER);` // Will replace with actual placeholders WHERE library_artist.artist_id IN (ROLE_IDS_PLACEHOLDER);` // Will replace with actual placeholders

View File

@ -1,7 +1,12 @@
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { createHashHistory } from 'history' import { createHashHistory } from 'history'
import { Admin as RAAdmin, Resource } from 'react-admin' import {
Admin as RAAdmin,
Resource,
useSetLocale,
useRefresh,
} from 'react-admin'
import { HotKeys } from 'react-hotkeys' import { HotKeys } from 'react-hotkeys'
import dataProvider from './dataProvider' import dataProvider from './dataProvider'
import authProvider from './authProvider' import authProvider from './authProvider'
@ -34,7 +39,7 @@ import {
shareDialogReducer, shareDialogReducer,
} from './reducers' } from './reducers'
import createAdminStore from './store/createAdminStore' import createAdminStore from './store/createAdminStore'
import { i18nProvider } from './i18n' import { i18nProvider, retrieveTranslation } from './i18n'
import config, { shareInfo } from './config' import config, { shareInfo } from './config'
import { keyMap } from './hotkeys' import { keyMap } from './hotkeys'
import useChangeThemeColor from './useChangeThemeColor' import useChangeThemeColor from './useChangeThemeColor'
@ -42,6 +47,7 @@ import SharePlayer from './share/SharePlayer'
import { HTML5Backend } from 'react-dnd-html5-backend' import { HTML5Backend } from 'react-dnd-html5-backend'
import { DndProvider } from 'react-dnd' import { DndProvider } from 'react-dnd'
import missing from './missing/index.js' import missing from './missing/index.js'
import { useEffect } from 'react'
const history = createHashHistory() const history = createHashHistory()
@ -81,6 +87,24 @@ const App = () => (
) )
const Admin = (props) => { const Admin = (props) => {
const setLocale = useSetLocale()
const refresh = useRefresh()
useEffect(() => {
if (config.defaultLanguage !== '' && !localStorage.getItem('locale')) {
retrieveTranslation(config.defaultLanguage)
.then(() => setLocale(config.defaultLanguage))
.then(() => {
localStorage.setItem('locale', config.defaultLanguage)
refresh(true)
})
.catch((e) => {
// eslint-disable-next-line no-console
console.error(
'Cannot load language "' + config.defaultLanguage + '": ' + e,
)
})
}
}, [setLocale, refresh])
useChangeThemeColor() useChangeThemeColor()
/* eslint-disable react/jsx-key */ /* eslint-disable react/jsx-key */
return ( return (

View File

@ -127,6 +127,7 @@ const Player = () => {
/> />
), ),
locale: locale(translate), locale: locale(translate),
sortableOptions: { delay: 200, delayOnTouchOnly: true },
}), }),
[gainInfo, isDesktop, playerTheme, translate, playerState.mode], [gainInfo, isDesktop, playerTheme, translate, playerState.mode],
) )

View File

@ -1,4 +1,4 @@
import React, { useState, useCallback, useEffect } from 'react' import React, { useState, useCallback } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Field, Form } from 'react-final-form' import { Field, Form } from 'react-final-form'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
@ -13,8 +13,6 @@ import {
createMuiTheme, createMuiTheme,
useLogin, useLogin,
useNotify, useNotify,
useRefresh,
useSetLocale,
useTranslate, useTranslate,
useVersion, useVersion,
} from 'react-admin' } from 'react-admin'
@ -24,7 +22,6 @@ import Notification from './Notification'
import useCurrentTheme from '../themes/useCurrentTheme' import useCurrentTheme from '../themes/useCurrentTheme'
import config from '../config' import config from '../config'
import { clearQueue } from '../actions' import { clearQueue } from '../actions'
import { retrieveTranslation } from '../i18n'
import { INSIGHTS_DOC_URL } from '../consts.js' import { INSIGHTS_DOC_URL } from '../consts.js'
const useStyles = makeStyles( const useStyles = makeStyles(
@ -400,27 +397,8 @@ Login.propTypes = {
// the right theme // the right theme
const LoginWithTheme = (props) => { const LoginWithTheme = (props) => {
const theme = useCurrentTheme() const theme = useCurrentTheme()
const setLocale = useSetLocale()
const refresh = useRefresh()
const version = useVersion() const version = useVersion()
useEffect(() => {
if (config.defaultLanguage !== '' && !localStorage.getItem('locale')) {
retrieveTranslation(config.defaultLanguage)
.then(() => {
setLocale(config.defaultLanguage).then(() => {
localStorage.setItem('locale', config.defaultLanguage)
})
refresh(true)
})
.catch((e) => {
throw new Error(
'Cannot load language "' + config.defaultLanguage + '": ' + e,
)
})
}
}, [refresh, setLocale])
return ( return (
<ThemeProvider theme={createMuiTheme(theme)}> <ThemeProvider theme={createMuiTheme(theme)}>
<Login key={version} {...props} /> <Login key={version} {...props} />