From a8f97dbc52a6a8e796f42fc66ae3eb68e460a2c8 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 22:18:43 +0500 Subject: [PATCH 1/4] upgrade react --- js/webui/package.json | 7 +++--- js/webui/src/hooks.js | 3 +-- js/webui/src/index.js | 7 +++--- js/webui/src/sandbox/index.js | 6 ++--- js/yarn.lock | 41 ++++++++++++----------------------- 5 files changed, 25 insertions(+), 39 deletions(-) diff --git a/js/webui/package.json b/js/webui/package.json index ffef46ca..5625fa59 100644 --- a/js/webui/package.json +++ b/js/webui/package.json @@ -22,11 +22,10 @@ "normalize.css": "^8.0.1", "open-iconic": "^1.1.1", "prop-types": "^15.8.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^19.2.3", + "react-dom": "^19.2.3", "react-modal": "^3.16.1", - "shallowequal": "^1.1.0", - "use-sync-external-store": "^1.6.0" + "shallowequal": "^1.1.0" }, "devDependencies": { "@babel/core": "^7.20.12", diff --git a/js/webui/src/hooks.js b/js/webui/src/hooks.js index d57c65d8..ffcdf33e 100644 --- a/js/webui/src/hooks.js +++ b/js/webui/src/hooks.js @@ -1,7 +1,6 @@ -import { useCallback, useContext, useEffect, useRef } from 'react'; +import { useCallback, useContext, useEffect, useRef, useSyncExternalStore } from 'react'; import shallowEqual from 'shallowequal'; import ServiceContext from './service_context.js'; -import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { subscribeAll } from './model_base.js'; export function useServices() diff --git a/js/webui/src/index.js b/js/webui/src/index.js index 9af4fff5..67ee8e64 100644 --- a/js/webui/src/index.js +++ b/js/webui/src/index.js @@ -9,6 +9,7 @@ import { PlaybackState } from 'beefweb-client'; import { SettingsView, View } from './navigation_model.js'; import { NotificationContainer } from './notification_container.js'; import { Router } from './router.js'; +import { createRoot } from 'react-dom/client'; const appModel = new AppModel(); @@ -158,7 +159,9 @@ async function main() fileBrowserModel.reload(); } - const appComponent = ( + const root = createRoot(document.getElementById('app-container')); + + root.render( @@ -166,8 +169,6 @@ async function main() ); - - ReactDom.render(appComponent, document.getElementById('app-container')); } main(); diff --git a/js/webui/src/sandbox/index.js b/js/webui/src/sandbox/index.js index 8b2ad2f1..2a32c022 100644 --- a/js/webui/src/sandbox/index.js +++ b/js/webui/src/sandbox/index.js @@ -4,6 +4,7 @@ import { mapRange } from '../utils.js' import DataTable from '../data_table.js' import { Menu, MenuLabel } from '../elements.js'; import { DialogButton } from '../dialogs.js'; +import { createRoot } from 'react-dom/client'; function createRow(index) { @@ -86,6 +87,5 @@ class Sandbox extends React.PureComponent document.title = 'Sandbox'; -ReactDom.render( - , - document.getElementById('app-container')); +const root = createRoot(document.getElementById('app-container')); +root.render(); diff --git a/js/yarn.lock b/js/yarn.lock index 4e283688..b6ec243e 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -1453,7 +1453,7 @@ lodash@^4.17.20, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -2005,14 +2005,12 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -react-dom@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== +react-dom@^19.2.3: + version "19.2.3" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17" + integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg== dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" + scheduler "^0.27.0" react-is@^16.13.1: version "16.13.1" @@ -2034,13 +2032,10 @@ react-modal@^3.16.1: react-lifecycles-compat "^3.0.0" warning "^4.0.3" -react@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" +react@^19.2.3: + version "19.2.3" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8" + integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA== rechoir@^0.7.0: version "0.7.1" @@ -2113,13 +2108,10 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== schema-utils@^2.6.5: version "2.7.1" @@ -2394,11 +2386,6 @@ url-loader@^4.1.1: mime-types "^2.1.27" schema-utils "^3.0.0" -use-sync-external-store@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" - integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== - util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From 00c5927b044771eb3ebd783298fb6e9ed92d6aec Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 10 Jan 2026 06:39:03 +0500 Subject: [PATCH 2/4] eliminate forwardRef calls --- js/webui/src/elements.js | 14 +++++++------- js/webui/src/playlist_switcher.js | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/js/webui/src/elements.js b/js/webui/src/elements.js index b76f7be8..ef1a310e 100644 --- a/js/webui/src/elements.js +++ b/js/webui/src/elements.js @@ -1,4 +1,4 @@ -import React, { forwardRef, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types' import spriteSvg from 'open-iconic/sprite/sprite.svg' import { generateElementId, makeClassName } from './dom_utils.js'; @@ -19,9 +19,9 @@ function makeClickHandler(callback) }; } -export const Icon = forwardRef(function Icon(props, ref) +function Icon(props) { - const { name, className } = props; + const { name, className, ref } = props; const fullClassName = 'icon icon-' + name + (className ? ' ' + className : ''); if (name === 'none') @@ -34,16 +34,16 @@ export const Icon = forwardRef(function Icon(props, ref) ); -}); +} Icon.propTypes = { name: PropTypes.string.isRequired, className: PropTypes.string, }; -export const IconButton = React.forwardRef(function IconButton(props, ref) +export function IconButton(props) { - const { name, title, className, href, onClick, active } = props; + const { name, title, className, href, onClick, active, ref } = props; const fullClassName = 'icon-button' + (className ? ' ' + className : '') @@ -59,7 +59,7 @@ export const IconButton = React.forwardRef(function IconButton(props, ref) ); -}); +} IconButton.propTypes = { name: PropTypes.string.isRequired, diff --git a/js/webui/src/playlist_switcher.js b/js/webui/src/playlist_switcher.js index be606217..bcd557ee 100644 --- a/js/webui/src/playlist_switcher.js +++ b/js/webui/src/playlist_switcher.js @@ -1,4 +1,4 @@ -import React, { forwardRef, useCallback, useLayoutEffect, useRef } from 'react'; +import React, { useCallback, useLayoutEffect, useRef } from 'react'; import PropTypes from 'prop-types' import { PlaybackState } from 'beefweb-client' import { Select } from './elements.js'; @@ -45,9 +45,9 @@ export function PlaylistSelector() ; } -const PlaylistTab = forwardRef(function PlaylistTab(props, ref) +function PlaylistTab(props, ref) { - const { playlist, isCurrent, isActive } = props; + const { playlist, isCurrent, isActive, ref } = props; const { attributes, @@ -84,7 +84,7 @@ const PlaylistTab = forwardRef(function PlaylistTab(props, ref) ); -}); +} PlaylistTab.propTypes = { playlist: PropTypes.object.isRequired, From 01bcde4a1744d6e9affa478d38a4f6463991d651 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 10 Jan 2026 07:01:12 +0500 Subject: [PATCH 3/4] index: use shorter context provider syntax --- js/webui/src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/webui/src/index.js b/js/webui/src/index.js index 67ee8e64..9b73b93b 100644 --- a/js/webui/src/index.js +++ b/js/webui/src/index.js @@ -163,10 +163,10 @@ async function main() root.render( - + - + ); } From e22f2b749641c9cc4a4c7b65efdd3aba57fade8b Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 10 Jan 2026 07:15:45 +0500 Subject: [PATCH 4/4] window_controller: convert to component, move NotificationContainer to App --- js/webui/sources.cmake | 2 +- js/webui/src/app.js | 22 +++++++++++++-------- js/webui/src/app_model.js | 3 --- js/webui/src/index.js | 1 - js/webui/src/window_controller.js | 32 ------------------------------- js/webui/src/window_title.js | 27 ++++++++++++++++++++++++++ 6 files changed, 42 insertions(+), 45 deletions(-) delete mode 100644 js/webui/src/window_controller.js create mode 100644 js/webui/src/window_title.js diff --git a/js/webui/sources.cmake b/js/webui/sources.cmake index 90b335ca..83b631f5 100644 --- a/js/webui/sources.cmake +++ b/js/webui/sources.cmake @@ -72,5 +72,5 @@ src/utils.js src/view_switcher.js src/view_switcher_controller.js src/volume_control.js -src/window_controller.js +src/window_title.js ) diff --git a/js/webui/src/app.js b/js/webui/src/app.js index 1341bd3b..e2627e88 100644 --- a/js/webui/src/app.js +++ b/js/webui/src/app.js @@ -14,6 +14,8 @@ import { PlaybackInfoBar } from './playback_info_bar.js'; import AlbumArtViewer from "./album_art_viewer.js"; import { useCurrentView, useSettingValue } from './hooks.js'; import { MediaSize } from './settings_model.js'; +import { WindowTitle } from './window_title.js'; +import { NotificationContainer } from './notification_container.js'; const viewContent = { [View.playlist]: mediaSize => { @@ -68,13 +70,17 @@ export function App() : null; return ( -
- {playbackInfoBar} - {upperControlBar} - {header} - {main} - {lowerControlBar} - {statusBar} -
+ <> + +
+ {playbackInfoBar} + {upperControlBar} + {header} + {main} + {lowerControlBar} + {statusBar} +
+ + ); } diff --git a/js/webui/src/app_model.js b/js/webui/src/app_model.js index bf82e4a7..3b60a886 100644 --- a/js/webui/src/app_model.js +++ b/js/webui/src/app_model.js @@ -13,7 +13,6 @@ import ColumnsSettingsModel from './columns_settings_model.js'; import PlayQueueModel from './play_queue_model.js'; import OutputSettingsModel from './output_settings_model.js'; import ViewSwitcherController from './view_switcher_controller.js'; -import WindowController from './window_controller.js'; import MediaSizeController from './media_size_controller.js'; import MediaThemeController from './media_theme_controller.js'; import TouchModeController from './touch_mode_controller.js'; @@ -43,7 +42,6 @@ export default class AppModel this.mediaThemeController = new MediaThemeController(this.settingsModel); this.touchModeController = new TouchModeController(this.settingsModel); this.cssSettingsController = new CssSettingsController(this.settingsModel); - this.windowController = new WindowController(this.playerModel); Object.freeze(this); } @@ -63,7 +61,6 @@ export default class AppModel this.touchModeController.start(); this.cssSettingsController.start(); this.columnsSettingsModel.start(); - this.windowController.start(); this.viewSwitcherController.start(); } } diff --git a/js/webui/src/index.js b/js/webui/src/index.js index 9b73b93b..dc52f614 100644 --- a/js/webui/src/index.js +++ b/js/webui/src/index.js @@ -165,7 +165,6 @@ async function main() - ); diff --git a/js/webui/src/window_controller.js b/js/webui/src/window_controller.js deleted file mode 100644 index 701232e0..00000000 --- a/js/webui/src/window_controller.js +++ /dev/null @@ -1,32 +0,0 @@ -import { PlaybackState } from 'beefweb-client' - -const stateToIcon = { - [PlaybackState.playing]: '\u25B6\uFE0F', - [PlaybackState.paused]: '\u23F8\uFE0F' -}; - -export default class WindowController -{ - constructor(playerModel) - { - this.playerModel = playerModel; - this.handleUpdate = this.handleUpdate.bind(this); - } - - start() - { - this.playerModel.on('change', this.handleUpdate); - this.handleUpdate(); - } - - handleUpdate() - { - const model = this.playerModel; - const state = model.playbackState; - - window.document.title = - state === PlaybackState.stopped - ? model.info.title - : stateToIcon[state] + ' ' + model.activeItem.columns[0] + ' - ' + model.info.title; - } -} diff --git a/js/webui/src/window_title.js b/js/webui/src/window_title.js new file mode 100644 index 00000000..a790501a --- /dev/null +++ b/js/webui/src/window_title.js @@ -0,0 +1,27 @@ +import { PlaybackState } from 'beefweb-client' +import { defineModelData } from './hooks.js'; + +const stateToIcon = { + [PlaybackState.playing]: '\u25B6\uFE0F', + [PlaybackState.paused]: '\u23F8\uFE0F' +}; + +const useWindowTitle = defineModelData({ + selector(context) + { + const { playbackState, info, activeItem } = context.playerModel; + return playbackState === PlaybackState.stopped + ? info.title + : stateToIcon[playbackState] + ' ' + activeItem.columns[0] + ' - ' + info.title; + }, + + updateOn: { + playerModel: 'change' + } +}); + +export function WindowTitle() +{ + const title = useWindowTitle(); + return {title} +}