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/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/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/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..dc52f614 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,16 +159,15 @@ async function main()
fileBrowserModel.reload();
}
- const appComponent = (
+ const root = createRoot(document.getElementById('app-container'));
+
+ root.render(
-
+
-
-
+
);
-
- ReactDom.render(appComponent, document.getElementById('app-container'));
}
main();
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,
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/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}
+}
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"