From 29af16f857af78a5524f97a3eec5fe6fd44b8f3e Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 17:19:49 +0500 Subject: [PATCH 01/12] add dnd-kit --- js/webui/package.json | 2 ++ js/yarn.lock | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/js/webui/package.json b/js/webui/package.json index b4577751..ef4ed7f5 100644 --- a/js/webui/package.json +++ b/js/webui/package.json @@ -13,6 +13,8 @@ "watch": "webpack-cli --watch" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "detect-it": "^4.0.1", "lodash": "^4.17.21", "navigo": "^8.11.1", diff --git a/js/yarn.lock b/js/yarn.lock index 18849be3..0f71e087 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -266,6 +266,37 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@dnd-kit/accessibility@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz#3b4202bd6bb370a0730f6734867785919beac6af" + integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw== + dependencies: + tslib "^2.0.0" + +"@dnd-kit/core@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.3.1.tgz#4c36406a62c7baac499726f899935f93f0e6d003" + integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ== + dependencies: + "@dnd-kit/accessibility" "^3.1.1" + "@dnd-kit/utilities" "^3.2.2" + tslib "^2.0.0" + +"@dnd-kit/sortable@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz#1f9382b90d835cd5c65d92824fa9dafb78c4c3e8" + integrity sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg== + dependencies: + "@dnd-kit/utilities" "^3.2.2" + tslib "^2.0.0" + +"@dnd-kit/utilities@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b" + integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg== + dependencies: + tslib "^2.0.0" + "@jridgewell/gen-mapping@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" @@ -2357,6 +2388,11 @@ totalist@^1.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== +tslib@^2.0.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@^2.0.3, tslib@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" From 798a1f68766d245adbbe73eba58455a51a16f2ac Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 17:25:38 +0500 Subject: [PATCH 02/12] column_settings_model: use ids instead of indices --- js/webui/src/columns_settings_model.js | 31 ++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/js/webui/src/columns_settings_model.js b/js/webui/src/columns_settings_model.js index b985a049..0f49e358 100644 --- a/js/webui/src/columns_settings_model.js +++ b/js/webui/src/columns_settings_model.js @@ -56,6 +56,16 @@ export default class ColumnsSettingsModel extends ModelBase this.revertChanges(); } + getColumn(id) + { + return this.columns.find(c => c.id === id); + } + + getColumnIndex(id) + { + return this.columns.findIndex(c => c.id === id); + } + setLayout(mediaSize) { this.layout = mediaSize; @@ -72,8 +82,8 @@ export default class ColumnsSettingsModel extends ModelBase setColumns(columns) { - this.columns = columns; this.config[this.layout].columns = columns; + this.columns = columns; this.emit('change'); } @@ -108,20 +118,33 @@ export default class ColumnsSettingsModel extends ModelBase this.setColumns([... this.columns, column]); } - updateColumn(index, patch) + updateColumn(id, patch) { + const index = this.getColumnIndex(id); + if (index < 0) + return; + const newColumns = [... this.columns]; newColumns[index] = Object.assign({}, this.columns[index], patch); this.setColumns(newColumns); } - moveColumn(oldIndex, newIndex) + moveColumn(oldId, newId) { + const oldIndex = this.getColumnIndex(oldId); + const newIndex = this.getColumnIndex(newId); + if (oldIndex < 0 || newIndex < 0) + return; + this.setColumns(arrayMove(this.columns, oldIndex, newIndex)); } - removeColumn(index) + removeColumn(id) { + const index = this.getColumnIndex(id); + if (index < 0) + return; + this.setColumns(arrayRemove(this.columns, index)); } } From 8dc4f5908361b9962e47d3d74c722f82156a171d Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 18:43:02 +0500 Subject: [PATCH 03/12] column_settings: rewrite using hooks --- js/webui/src/columns_settings.js | 562 ++++++++++--------------- js/webui/src/columns_settings_model.js | 6 +- js/webui/src/hooks.js | 27 +- js/webui/src/settings_content.js | 2 +- 4 files changed, 264 insertions(+), 333 deletions(-) diff --git a/js/webui/src/columns_settings.js b/js/webui/src/columns_settings.js index 21405948..762eb4d3 100644 --- a/js/webui/src/columns_settings.js +++ b/js/webui/src/columns_settings.js @@ -1,13 +1,10 @@ -import React from 'react'; +import React, { useCallback, useState } from 'react'; import PropTypes from 'prop-types' -import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc'; -import { bindHandlers } from './utils.js'; import { IconButton, Icon, Select } from './elements.js'; import ReactModal from 'react-modal'; import { ConfirmDialog, DialogButton } from './dialogs.js'; -import ModelBinding from './model_binding.js'; -import ServiceContext from "./service_context.js"; import { ColumnAlign } from './columns.js'; +import { defineKeyedModelData, defineModelData, useServices } from './hooks.js'; const AlignItems = [ { id: ColumnAlign.left, name: 'Left' }, @@ -15,364 +12,273 @@ const AlignItems = [ { id: ColumnAlign.right, name: 'Right' }, ]; -class ColumnEditorDialog extends React.PureComponent -{ - constructor(props) - { - super(props); - this.state = { }; - bindHandlers(this); - } - - update(patch) - { - this.props.onUpdate(patch); - } - - handleTitleChange(e) - { - this.update({ title: e.target.value }); - } - - handleExpressionChange(e) - { - this.update({ expression: e.target.value }); - } - - handleSizeChange(e) - { - const value = Number(e.target.value); - - if (!isNaN(value) && value >= 0) - this.update({ size: value }); +const useColumnList = defineModelData({ + selector: context => context.columnsSettingsModel.columns, + updateOn: { + columnsSettingsModel: 'change' } +}); - handleAlignChange(e) - { - this.update({ align: e.target.value }); +const useColumn = defineKeyedModelData({ + selector: (context, columnId) => context.columnsSettingsModel.getColumn(columnId), + updateOn: { + columnsSettingsModel: 'change' } +}); - handleBoldChange(e) - { - this.update({ bold: e.target.checked }); - } - - handleItalicChange(e) - { - this.update({ italic: e.target.checked }); - } - - handleSmallChange(e) - { - this.update({ small: e.target.checked }); - } - - render() - { - const { isOpen, column, onOk, onCancel } = this.props; - - return ( - -
-
Edit column
-
-
- - -
-
- - -
-
- - setColumnData({ ...columnData, title: e.target.value }), + callbackDeps); + + const changeExpression = useCallback( + e => setColumnData({ ...columnData, expression: e.target.value }), + callbackDeps); + + const changeSize = useCallback( + e => { + const value = Number(e.target.value); + + if (!isNaN(value) && value >= 0) + setColumnData({ ...columnData, size: value }); + }, + callbackDeps); + + const changeAlign = useCallback( + e => setColumnData({ ...columnData, align: e.target.value }), + callbackDeps); + + const changeBold = useCallback( + e => setColumnData({ ...columnData, bold: e.target.checked }), + callbackDeps); + + const changeItalic = useCallback( + e => setColumnData({ ...columnData, italic: e.target.checked }), + callbackDeps); + + const changeSmall = useCallback( + e => setColumnData({ ...columnData, small: e.target.checked }), + callbackDeps); + + const handleOpen = useCallback( + () => { + setColumnData({ ...model.getColumn(columnId) }); + setDialogOpen(true); + }, + callbackDeps) + + const handleOk = useCallback( + () => { + model.updateColumn(columnData); + setColumnData(null); + setDialogOpen(false); + }, + callbackDeps); + + const handleCancel = useCallback( + () => { + setColumnData(null); + setDialogOpen(false); + }, + callbackDeps); + + const icon = ; + + if (!dialogOpen) + return icon; + + return <> + {icon} + + +
Edit column
+
+
+ + +
+
+ + +
+
+ + +
+
+ + -
-
- Font attributes: -
- - - -
-
+ items={AlignItems} + onChange={changeAlign} + selectedItemId={columnData.align}/>
-
- - +
+ Font attributes: +
+ + + +
- - - ); - } +
+
+ + +
+ +
+ ; } -ColumnEditorDialog.propTypes = { - isOpen: PropTypes.bool.isRequired, - column: PropTypes.object.isRequired, - onOk: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, - onUpdate: PropTypes.func.isRequired, +ColumnEditButton.propTypes = { + columnId: PropTypes.number.isRequired, }; -function ColumnEditorDragHandle_() +function ColumnDeleteButton(props) { - return ; + const { columnId } = props; + const model = useServices().columnsSettingsModel; + const [dialogOpen, setDialogOpen] = useState(false); + const column = useColumn(columnId); + + const handleOpen = useCallback(() => setDialogOpen(true), []); + + const handleOk = useCallback( + () => { + model.removeColumn(columnId) + setDialogOpen(false); + }, + [columnId]); + + const handleCancel = useCallback(() => setDialogOpen(false), []) + + const icon = ; + + if (!dialogOpen) + return icon; + + const columnName = column.lineBreak ? 'line break' : `column ${column.title}`; + + return <> + {icon} + + } -const ColumnEditorDragHandle = SortableHandle(ColumnEditorDragHandle_); +ColumnDeleteButton.propTypes = { + columnId: PropTypes.number.isRequired, +}; -class ColumnEditor_ extends React.PureComponent +function ColumnDragHandle() { - constructor(props) - { - super(props); - - this.state = Object.assign( - { deleteDialogOpen: false }, ColumnEditor_.editDialogClosed()); - - bindHandlers(this); - } - - static editDialogClosed() - { - return { - editDialogOpen: false, - editedColumn: { - title: '', - expression: '', - size: 1 - } - }; - } - - handleEdit() - { - this.setState({ - editDialogOpen: true, - editedColumn: structuredClone(this.props.column), - }); - } + return ; +} - handleEditOk() - { - this.props.onUpdate(this.props.columnIndex, this.state.editedColumn); - this.setState(ColumnEditor_.editDialogClosed); - } +function EditableColumn(props) +{ + const { columnId } = props; + const column = useColumn(columnId); - handleEditCancel() - { - this.setState(ColumnEditor_.editDialogClosed); - } + let editButton; + let columnInfo; - handleEditUpdate(patch) + if (column.lineBreak) { - this.setState(state => ({ editedColumn: Object.assign({}, state.editedColumn, patch) })); + columnInfo =
+ {'\u2E3A Line break \u2E3A'} +
; } - - handleDelete() + else { - this.setState({ deleteDialogOpen: true }); - } + columnInfo =
+ { column.title } + { column.expression } +
; - handleDeleteOk() - { - this.props.onDelete(this.props.columnIndex); - this.setState({ deleteDialogOpen: false }); + editButton = ; } - handleDeleteCancel() - { - this.setState({ deleteDialogOpen: false }); - } - - render() - { - const { column } = this.props; - const { deleteDialogOpen, editDialogOpen, editedColumn } = this.state; - - let editButton; - let columnInfo; - let columnName; - - if (column.lineBreak) - { - columnInfo =
- {'\u2E3A Line break \u2E3A'} -
; - - columnName = 'line break'; - } - else - { - columnInfo =
- { column.title } - { column.expression } -
; - - editButton = ; - columnName = `column ${column.title}`; - } - - return ( -
-
- -
- { columnInfo } -
-
- { editButton } - -
+ return ( +
+
+ +
+ { columnInfo } +
+
+ { editButton } +
- -
- ); - } +
+ ); } -ColumnEditor_.propTypes = { - columnIndex: PropTypes.number.isRequired, - column: PropTypes.object.isRequired, - onUpdate: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, +EditableColumn.propTypes = { + columnId: PropTypes.number.isRequired, }; -const ColumnEditor = SortableElement(ColumnEditor_); - -class ColumnEditorList_ extends React.PureComponent +function EditableColumnList() { - static contextType = ServiceContext; - - constructor(props, context) - { - super(props, context); - this.state = this.getStateFromModel(); - bindHandlers(this); - } - - getStateFromModel() - { - const { columns } = this.context.columnsSettingsModel; - return { columns }; - } + const columns = useColumnList(); - handleUpdate(index, patch) - { - this.context.columnsSettingsModel.updateColumn(index, patch); - } - - handleDelete(index) - { - this.context.columnsSettingsModel.removeColumn(index); - } + const columnElements = columns.map(c => ( + + )); - render() - { - const editors = this.state.columns.map((c, i) => ( - - )); - - return ( -
- {editors} -
- ); - } + return ( +
+ {columnElements} +
+ ); } -const ColumnEditorList = SortableContainer(ModelBinding( - ColumnEditorList_, { columnsSettingsModel: 'change' })); - -export default class ColumnsSettings extends React.PureComponent +export function ColumnsSettings() { - static contextType = ServiceContext; - - constructor(props) - { - super(props); - - this.state = {}; - - bindHandlers(this); - } - - handleSortEnd(e) - { - this.context.columnsSettingsModel.moveColumn(e.oldIndex, e.newIndex); - } - - componentWillUnmount() - { - this.context.columnsSettingsModel.applyChanges(); - } - - render() - { - return ( - - ); - } + return ; } diff --git a/js/webui/src/columns_settings_model.js b/js/webui/src/columns_settings_model.js index 0f49e358..2c60cfb1 100644 --- a/js/webui/src/columns_settings_model.js +++ b/js/webui/src/columns_settings_model.js @@ -118,14 +118,14 @@ export default class ColumnsSettingsModel extends ModelBase this.setColumns([... this.columns, column]); } - updateColumn(id, patch) + updateColumn(column) { - const index = this.getColumnIndex(id); + const index = this.getColumnIndex(column.id); if (index < 0) return; const newColumns = [... this.columns]; - newColumns[index] = Object.assign({}, this.columns[index], patch); + newColumns[index] = structuredClone(column); this.setColumns(newColumns); } diff --git a/js/webui/src/hooks.js b/js/webui/src/hooks.js index b77d7ee6..87ca6860 100644 --- a/js/webui/src/hooks.js +++ b/js/webui/src/hooks.js @@ -1,4 +1,4 @@ -import { useCallback, useContext } from 'react'; +import { useCallback, useContext, useRef } from 'react'; import shallowEqual from 'shallowequal'; import ServiceContext from './service_context.js'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; @@ -36,6 +36,18 @@ function getSnapshot(context, selector) return newData; } +function getKeyedSnapshot(context, selector, store, key) +{ + const oldData = store.current && store.current.lastKey === key ? store.current.lastValue : null; + const newData = selector(context, key); + + if (shallowEqual(oldData, newData)) + return oldData; + + store.current = { lastKey: key, lastValue: newData }; + return newData; +} + export function defineModelData(arg) { const { selector, updateOn } = arg; @@ -48,6 +60,19 @@ export function defineModelData(arg) }; } +export function defineKeyedModelData(arg) +{ + const { selector, updateOn } = arg; + + return key => { + const context = useServices(); + const store = useRef(null); + const subscribe = useCallback(cb => subscribeAll(context, updateOn, cb), []); + const snapshot = useCallback(() => getKeyedSnapshot(context, selector, store, key), [key]); + return useSyncExternalStore(subscribe, snapshot); + }; +} + export const useCurrentView = defineModelData({ selector: context => context.navigationModel.view, updateOn: { navigationModel: 'viewChange' } diff --git a/js/webui/src/settings_content.js b/js/webui/src/settings_content.js index e63cc9b4..a310cd20 100644 --- a/js/webui/src/settings_content.js +++ b/js/webui/src/settings_content.js @@ -3,7 +3,7 @@ import { SettingsView } from './navigation_model.js'; import ModelBinding from './model_binding.js'; import DefaultsSettings from './defaults_settings.js' import GeneralSettings from './general_settings.js'; -import ColumnsSettings from './columns_settings.js'; +import { ColumnsSettings } from './columns_settings.js'; import ServiceContext from './service_context.js'; import AboutBox from './about_box.js'; import OutputSettings from './output_settings.js'; From 0a49d1246f06e555c807a8c10a796f7323f5d949 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 18:49:45 +0500 Subject: [PATCH 04/12] hooks: use shallow equal when comparing keys --- js/webui/src/hooks.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/webui/src/hooks.js b/js/webui/src/hooks.js index 87ca6860..cee46a75 100644 --- a/js/webui/src/hooks.js +++ b/js/webui/src/hooks.js @@ -38,7 +38,10 @@ function getSnapshot(context, selector) function getKeyedSnapshot(context, selector, store, key) { - const oldData = store.current && store.current.lastKey === key ? store.current.lastValue : null; + const oldData = store.current && shallowEqual(store.current.lastKey, key) + ? store.current.lastValue + : null; + const newData = selector(context, key); if (shallowEqual(oldData, newData)) From 3276d90951da185f10c9e393a249029b9def52b5 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 19:42:19 +0500 Subject: [PATCH 05/12] columns_settings: reimplement using dnd-kit --- js/webui/package.json | 1 + js/webui/src/columns_settings.js | 73 ++++++++++++++++++-------- js/webui/src/columns_settings_model.js | 2 +- js/webui/src/elements.js | 8 +-- js/webui/src/hooks.js | 7 ++- 5 files changed, 64 insertions(+), 27 deletions(-) diff --git a/js/webui/package.json b/js/webui/package.json index ef4ed7f5..1f5399cb 100644 --- a/js/webui/package.json +++ b/js/webui/package.json @@ -15,6 +15,7 @@ "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "detect-it": "^4.0.1", "lodash": "^4.17.21", "navigo": "^8.11.1", diff --git a/js/webui/src/columns_settings.js b/js/webui/src/columns_settings.js index 762eb4d3..ed6ea945 100644 --- a/js/webui/src/columns_settings.js +++ b/js/webui/src/columns_settings.js @@ -4,7 +4,10 @@ import { IconButton, Icon, Select } from './elements.js'; import ReactModal from 'react-modal'; import { ConfirmDialog, DialogButton } from './dialogs.js'; import { ColumnAlign } from './columns.js'; -import { defineKeyedModelData, defineModelData, useServices } from './hooks.js'; +import { defineKeyedModelData, defineModelData, useDispose, useServices } from './hooks.js'; +import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; const AlignItems = [ { id: ColumnAlign.left, name: 'Left' }, @@ -214,16 +217,24 @@ ColumnDeleteButton.propTypes = { columnId: PropTypes.number.isRequired, }; -function ColumnDragHandle() -{ - return ; -} - function EditableColumn(props) { const { columnId } = props; const column = useColumn(columnId); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ id: columnId }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + let editButton; let columnInfo; @@ -244,9 +255,9 @@ function EditableColumn(props) } return ( -
+
- +
{ columnInfo }
@@ -263,22 +274,42 @@ EditableColumn.propTypes = { columnId: PropTypes.number.isRequired, }; -function EditableColumnList() +export function ColumnsSettings() { + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + distance: 10, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 5, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const model = useServices().columnsSettingsModel; const columns = useColumnList(); + const columnElements = columns.map(c => ); - const columnElements = columns.map(c => ( - - )); + const handleDragEnd = useCallback(event => { + const { active, over } = event; + if (active.id !== over.id) + model.moveColumn(active.id, over.id); + }, []); - return ( -
- {columnElements} -
- ); -} + useDispose(() => model.applyChanges()); -export function ColumnsSettings() -{ - return ; + return + +
+ {columnElements} +
+
+
; } diff --git a/js/webui/src/columns_settings_model.js b/js/webui/src/columns_settings_model.js index 2c60cfb1..d3abfcc9 100644 --- a/js/webui/src/columns_settings_model.js +++ b/js/webui/src/columns_settings_model.js @@ -1,4 +1,4 @@ -import { arrayMove } from 'react-sortable-hoc'; +import { arrayMove } from '@dnd-kit/sortable'; import { arrayRemove } from './utils.js'; import ModelBase from './model_base.js'; import { MediaSize } from './settings_model.js'; diff --git a/js/webui/src/elements.js b/js/webui/src/elements.js index 57828769..b76f7be8 100644 --- a/js/webui/src/elements.js +++ b/js/webui/src/elements.js @@ -1,4 +1,4 @@ -import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { forwardRef, 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,7 +19,7 @@ function makeClickHandler(callback) }; } -export function Icon(props) +export const Icon = forwardRef(function Icon(props, ref) { const { name, className } = props; const fullClassName = 'icon icon-' + name + (className ? ' ' + className : ''); @@ -30,11 +30,11 @@ export function Icon(props) const href = `${spriteSvg}#${name}`; return ( - + ); -} +}); Icon.propTypes = { name: PropTypes.string.isRequired, diff --git a/js/webui/src/hooks.js b/js/webui/src/hooks.js index cee46a75..1a7e0a91 100644 --- a/js/webui/src/hooks.js +++ b/js/webui/src/hooks.js @@ -1,4 +1,4 @@ -import { useCallback, useContext, useRef } from 'react'; +import { useCallback, useContext, useEffect, useRef } from 'react'; import shallowEqual from 'shallowequal'; import ServiceContext from './service_context.js'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; @@ -9,6 +9,11 @@ export function useServices() return useContext(ServiceContext); } +export function useDispose(callback) +{ + return useEffect(() => callback, []); +} + export function useSettingValue(settingName) { const { settingsModel } = useServices(); From b56f78650f2d5df9bd543cf9398c63b63592ab16 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 20:43:20 +0500 Subject: [PATCH 06/12] playlist_switcher: reimplement using function components --- js/webui/src/app.js | 19 ++- js/webui/src/hooks.js | 5 + js/webui/src/playlist_switcher.js | 237 ++++++++++-------------------- 3 files changed, 90 insertions(+), 171 deletions(-) diff --git a/js/webui/src/app.js b/js/webui/src/app.js index 2ed75181..1341bd3b 100644 --- a/js/webui/src/app.js +++ b/js/webui/src/app.js @@ -1,7 +1,7 @@ import React from 'react' import { PanelHeader } from './elements.js' import { ControlBarNarrowCompact, ControlBarNarrowFull, ControlBarWide } from './control_bar.js' -import PlaylistSwitcher from './playlist_switcher.js' +import { PlaylistSelector, TabbedPlaylistSwitcher } from './playlist_switcher.js' import PlaylistMenu from './playlist_menu.js' import PlaylistContent from './playlist_content.js' import FileBrowser from './file_browser.js' @@ -16,12 +16,14 @@ import { useCurrentView, useSettingValue } from './hooks.js'; import { MediaSize } from './settings_model.js'; const viewContent = { - [View.playlist]: { - header:
- - -
, - main: , + [View.playlist]: mediaSize => { + return { + header:
+ { mediaSize === MediaSize.small ? : } + +
, + main: , + }; }, [View.fileBrowser]: { header: , @@ -47,7 +49,8 @@ export function App() const showStatusBar = useSettingValue('showStatusBar'); const mediaSize = useSettingValue('mediaSize'); - const { header, main } = viewContent[view]; + const contentProvider = viewContent[view]; + const { header, main } = typeof contentProvider === 'function' ? contentProvider(mediaSize) : contentProvider; const playbackInfoBar = mediaSize === MediaSize.small && showPlaybackInfo ? : null; const statusBar = showStatusBar ? : null; diff --git a/js/webui/src/hooks.js b/js/webui/src/hooks.js index 1a7e0a91..a00c2704 100644 --- a/js/webui/src/hooks.js +++ b/js/webui/src/hooks.js @@ -14,6 +14,11 @@ export function useDispose(callback) return useEffect(() => callback, []); } +export function usePlaylistModel() +{ + return useServices().playlistModel; +} + export function useSettingValue(settingName) { const { settingsModel } = useServices(); diff --git a/js/webui/src/playlist_switcher.js b/js/webui/src/playlist_switcher.js index 25f4311d..9e12e2b9 100644 --- a/js/webui/src/playlist_switcher.js +++ b/js/webui/src/playlist_switcher.js @@ -1,185 +1,96 @@ -import React from 'react' -import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc'; +import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useRef } from 'react'; +import PropTypes from 'prop-types' import { PlaybackState } from 'beefweb-client' -import { Icon, Select } from './elements.js'; +import { Select } from './elements.js'; import urls from './urls.js' -import { bindHandlers } from './utils.js' import { makeClassName } from './dom_utils.js' -import ModelBinding from './model_binding.js'; -import ServiceContext from "./service_context.js"; -import { MediaSize } from './settings_model.js'; +import { defineModelData, usePlaylistModel } from './hooks.js'; -function PlaylistTabHandle_() -{ - return ( - - ); -} - -const PlaylistTabHandle = SortableHandle(PlaylistTabHandle_); - -class PlaylistTab_ extends React.PureComponent -{ - componentDidMount() +const usePlaylistsData = defineModelData({ + selector(context) { - const { playlist, currentPlaylistId } = this.props; + const { playerModel, playlistModel } = context; + const { playbackState, activeItem, permissions } = playerModel; + const { currentPlaylistId, playlists } = playlistModel; - if (playlist.id === currentPlaylistId) - this.element.scrollIntoView(); - } - - render() - { - const { - playlist: p, - playbackState, - activePlaylistId, + return { + activePlaylistId: playbackState !== PlaybackState.stopped ? activeItem.playlistId : null, currentPlaylistId, - drawHandle - } = this.props; - - const className = makeClassName({ - 'header-tab': true, - 'header-tab-with-icon': true, - 'header-tab-active': p.id === currentPlaylistId, - 'header-tab-playing': playbackState !== PlaybackState.stopped && p.id === activePlaylistId, - }); - - const handle = drawHandle ? : null; + playlists, + allowChange: permissions.changePlaylists, + }; + }, - return ( -
  • this.element = el}> - {handle} - - {p.title} - -
  • - ); + updateOn: { + playerModel: 'change', + playlistModel: 'playlistsChange', + settingsModel: ['touchMode', 'mediaSize'], } -} - -const PlaylistTab = SortableElement(PlaylistTab_); +}); -function PlaylistTabList_(props) +export function PlaylistSelector() { - const { - playbackState, - activePlaylistId, - currentPlaylistId, - playlists, - drawHandle, - disabled, - } = props; - - return ( -
      - { - playlists.map(p => ( - - )) - } -
    - ); + const model = usePlaylistModel(); + const data = usePlaylistsData(); + const setCurrentPlaylist = useCallback(e => model.setCurrentPlaylistId(e.target.value), []); + + return
    + -
    ; + playlists.map(p => ( + + )) } - - return ( - - ); - } -} - -export default ModelBinding(PlaylistSwitcher, { - playerModel: 'change', - playlistModel: 'playlistsChange', - settingsModel: ['touchMode', 'mediaSize'], -}); + +} \ No newline at end of file From f8c1fbc6e24b002cb20365aba9f78f904374f2ab Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 21:08:14 +0500 Subject: [PATCH 07/12] playlist_switcher: reimplement sorting --- js/webui/src/playlist_model.js | 27 ++++++----- js/webui/src/playlist_switcher.js | 78 +++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 26 deletions(-) diff --git a/js/webui/src/playlist_model.js b/js/webui/src/playlist_model.js index e62cc631..979c8fa3 100644 --- a/js/webui/src/playlist_model.js +++ b/js/webui/src/playlist_model.js @@ -1,4 +1,4 @@ -import { arrayMove } from 'react-sortable-hoc' +import { arrayMove } from '@dnd-kit/sortable'; import { AddAction } from "./settings_model.js"; import ModelBase from './model_base.js'; import { looseDeepEqual } from './utils.js'; @@ -153,9 +153,14 @@ export default class PlaylistModel extends ModelBase this.client.play(this.currentPlaylistId, index); } + getPlaylistIndex(id) + { + return this.playlists.findIndex(p => p.id === id); + } + getNewPlaylistTitle() { - var title = 'New Playlist'; + let title = 'New Playlist'; if (!this.playlists.find(p => p.title === title)) return title; @@ -169,18 +174,18 @@ export default class PlaylistModel extends ModelBase } } - movePlaylist(oldIndex, newIndex) + movePlaylist(oldId, newId) { - const oldPlaylists = this.playlists; - - if (oldIndex === newIndex - || oldIndex > oldPlaylists.length - || newIndex > oldPlaylists.length) + if (oldId === newId) return; - const playlistId = oldPlaylists[oldIndex].id; + const oldIndex = this.getPlaylistIndex(oldId); + const newIndex = this.getPlaylistIndex(newId); + + if (oldIndex < 0 || newIndex < 0) + return; - const newPlaylists = oldPlaylists.map(p => { + const newPlaylists = this.playlists.map(p => { if (p.index === oldIndex) return Object.assign({}, p, { index: newIndex }); else if (p.index === newIndex) @@ -189,7 +194,7 @@ export default class PlaylistModel extends ModelBase }); this.setPlaylists(arrayMove(newPlaylists, oldIndex, newIndex)); - this.client.movePlaylist(playlistId, newIndex); + this.client.movePlaylist(oldId, newIndex); } addPlaylist() diff --git a/js/webui/src/playlist_switcher.js b/js/webui/src/playlist_switcher.js index 9e12e2b9..36200aaa 100644 --- a/js/webui/src/playlist_switcher.js +++ b/js/webui/src/playlist_switcher.js @@ -1,10 +1,13 @@ -import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useRef } from 'react'; +import React, { forwardRef, useCallback, useLayoutEffect, useRef } from 'react'; import PropTypes from 'prop-types' import { PlaybackState } from 'beefweb-client' import { Select } from './elements.js'; import urls from './urls.js' import { makeClassName } from './dom_utils.js' import { defineModelData, usePlaylistModel } from './hooks.js'; +import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; const usePlaylistsData = defineModelData({ selector(context) @@ -47,6 +50,27 @@ const PlaylistTab = forwardRef(function PlaylistTab(props, ref) { const { playlist, isCurrent, isActive } = props; + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ id: playlist.id }); + + const setRef = useCallback( + value => { + setNodeRef(value); + if (ref) + ref.current = value; + }, + [setNodeRef, ref]) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + const className = makeClassName({ 'header-tab': true, 'header-tab-with-icon': true, @@ -55,7 +79,7 @@ const PlaylistTab = forwardRef(function PlaylistTab(props, ref) }); return ( -
  • +
  • {playlist.title} @@ -71,7 +95,25 @@ PlaylistTab.propTypes = { export function TabbedPlaylistSwitcher() { - const { playlists, activePlaylistId, currentPlaylistId } = usePlaylistsData(); + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + distance: 10, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 5, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const model = usePlaylistModel(); + const { playlists, activePlaylistId, currentPlaylistId, allowChange } = usePlaylistsData(); const currentPlaylistRef = useRef(); useLayoutEffect( @@ -81,16 +123,22 @@ export function TabbedPlaylistSwitcher() }, [currentPlaylistId]); - return
      - { - playlists.map(p => ( - - )) - } -
    + const handleDragEnd = useCallback(e => model.movePlaylist(e.active.id, e.over.id), []); + + const tabs = playlists.map(p => ( + + )); + + return + +
      + {tabs} +
    +
    +
    ; } \ No newline at end of file From b8123f302c22c37cfb2f3916fa8a22c55a960ff3 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 21:11:03 +0500 Subject: [PATCH 08/12] minor fixes --- js/webui/src/columns_settings.js | 14 +++++--------- js/webui/src/columns_settings_model.js | 4 ++++ js/webui/src/hooks.js | 5 +++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/js/webui/src/columns_settings.js b/js/webui/src/columns_settings.js index ed6ea945..c7d0a343 100644 --- a/js/webui/src/columns_settings.js +++ b/js/webui/src/columns_settings.js @@ -4,7 +4,7 @@ import { IconButton, Icon, Select } from './elements.js'; import ReactModal from 'react-modal'; import { ConfirmDialog, DialogButton } from './dialogs.js'; import { ColumnAlign } from './columns.js'; -import { defineKeyedModelData, defineModelData, useDispose, useServices } from './hooks.js'; +import { defineKeyedModelData, defineModelData, useColumnsSettingsModel, useDispose, useServices } from './hooks.js'; import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; import { SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; @@ -32,7 +32,7 @@ const useColumn = defineKeyedModelData({ function ColumnEditButton(props) { const { columnId } = props; - const model = useServices().columnsSettingsModel; + const model = useColumnsSettingsModel(); const [dialogOpen, setDialogOpen] = useState(false); const [columnData, setColumnData] = useState(null); const callbackDeps = [columnId, columnData]; @@ -180,7 +180,7 @@ ColumnEditButton.propTypes = { function ColumnDeleteButton(props) { const { columnId } = props; - const model = useServices().columnsSettingsModel; + const model = useColumnsSettingsModel(); const [dialogOpen, setDialogOpen] = useState(false); const column = useColumn(columnId); @@ -293,15 +293,11 @@ export function ColumnsSettings() }) ); - const model = useServices().columnsSettingsModel; + const model = useColumnsSettingsModel(); const columns = useColumnList(); const columnElements = columns.map(c => ); - const handleDragEnd = useCallback(event => { - const { active, over } = event; - if (active.id !== over.id) - model.moveColumn(active.id, over.id); - }, []); + const handleDragEnd = useCallback(e => model.moveColumn(e.active.id, e.over.id), []); useDispose(() => model.applyChanges()); diff --git a/js/webui/src/columns_settings_model.js b/js/webui/src/columns_settings_model.js index d3abfcc9..d115d1e4 100644 --- a/js/webui/src/columns_settings_model.js +++ b/js/webui/src/columns_settings_model.js @@ -131,8 +131,12 @@ export default class ColumnsSettingsModel extends ModelBase moveColumn(oldId, newId) { + if (oldId === newId) + return; + const oldIndex = this.getColumnIndex(oldId); const newIndex = this.getColumnIndex(newId); + if (oldIndex < 0 || newIndex < 0) return; diff --git a/js/webui/src/hooks.js b/js/webui/src/hooks.js index a00c2704..d57c65d8 100644 --- a/js/webui/src/hooks.js +++ b/js/webui/src/hooks.js @@ -19,6 +19,11 @@ export function usePlaylistModel() return useServices().playlistModel; } +export function useColumnsSettingsModel() +{ + return useServices().columnsSettingsModel; +} + export function useSettingValue(settingName) { const { settingsModel } = useServices(); From c360bd3e7992bbc90b322940dfad2f15c927fde7 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 21:13:42 +0500 Subject: [PATCH 09/12] remove react-sortable-hoc --- js/webui/package.json | 1 - js/yarn.lock | 30 +----------------------------- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/js/webui/package.json b/js/webui/package.json index 1f5399cb..ffef46ca 100644 --- a/js/webui/package.json +++ b/js/webui/package.json @@ -25,7 +25,6 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-modal": "^3.16.1", - "react-sortable-hoc": "^2.0.0", "shallowequal": "^1.1.0", "use-sync-external-store": "^1.6.0" }, diff --git a/js/yarn.lock b/js/yarn.lock index 0f71e087..4e283688 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -220,13 +220,6 @@ "@babel/plugin-transform-react-jsx-development" "^7.18.6" "@babel/plugin-transform-react-pure-annotations" "^7.18.6" -"@babel/runtime@^7.2.0": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" - integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== - dependencies: - regenerator-runtime "^0.13.11" - "@babel/template@^7.18.10", "@babel/template@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" @@ -1317,13 +1310,6 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" @@ -1984,7 +1970,7 @@ pretty-error@^4.0.0: lodash "^4.17.20" renderkid "^3.0.0" -prop-types@^15.5.7, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -2048,15 +2034,6 @@ react-modal@^3.16.1: react-lifecycles-compat "^3.0.0" warning "^4.0.3" -react-sortable-hoc@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz#f6780d8aa4b922a21f3e754af542f032677078b7" - integrity sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg== - dependencies: - "@babel/runtime" "^7.2.0" - invariant "^2.2.4" - prop-types "^15.5.7" - react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -2072,11 +2049,6 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" From 21483469abc4461c86754e59879c1099e7512b97 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 21:14:51 +0500 Subject: [PATCH 10/12] minor fixe --- js/webui/src/playlist_switcher.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/webui/src/playlist_switcher.js b/js/webui/src/playlist_switcher.js index 36200aaa..2ccc2007 100644 --- a/js/webui/src/playlist_switcher.js +++ b/js/webui/src/playlist_switcher.js @@ -27,7 +27,6 @@ const usePlaylistsData = defineModelData({ updateOn: { playerModel: 'change', playlistModel: 'playlistsChange', - settingsModel: ['touchMode', 'mediaSize'], } }); From aa20a9ccb2dd408953f4e7677c0445a0b3d7157a Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 21:30:37 +0500 Subject: [PATCH 11/12] column_settings: remove drag handle --- js/webui/src/columns_settings.js | 3 --- js/webui/src/style.less | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/js/webui/src/columns_settings.js b/js/webui/src/columns_settings.js index c7d0a343..bc04c4d1 100644 --- a/js/webui/src/columns_settings.js +++ b/js/webui/src/columns_settings.js @@ -256,9 +256,6 @@ function EditableColumn(props) return (
    -
    - -
    { columnInfo }
    diff --git a/js/webui/src/style.less b/js/webui/src/style.less index 1744d1f5..5243c3e8 100644 --- a/js/webui/src/style.less +++ b/js/webui/src/style.less @@ -994,6 +994,7 @@ body { display: flex; background: var(--color-background); max-width: 40rem; + cursor: grab; &:not(:last-child) { margin-bottom: 0.5rem; @@ -1030,7 +1031,6 @@ body { } } -.column-editor-drag-handle { - cursor: grab; +.column-editor-list { + overflow: hidden; } - From cc05523980ff29265cdc99e027c70a9e026760a6 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Fri, 9 Jan 2026 21:55:38 +0500 Subject: [PATCH 12/12] add helpers --- js/webui/sources.cmake | 2 ++ js/webui/src/columns_settings.js | 34 +++++++------------------- js/webui/src/playlist_switcher.js | 28 +++++----------------- js/webui/src/sortable.js | 40 +++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 47 deletions(-) create mode 100644 js/webui/src/sortable.js diff --git a/js/webui/sources.cmake b/js/webui/sources.cmake index 217c2475..90b335ca 100644 --- a/js/webui/sources.cmake +++ b/js/webui/sources.cmake @@ -25,6 +25,7 @@ src/file_browser.js src/file_browser_header.js src/file_browser_model.js src/general_settings.js +src/hooks.js src/index.html src/index.js src/loader.gif @@ -59,6 +60,7 @@ src/settings_header.js src/settings_model.js src/settings_model_base.js src/settings_store.js +src/sortable.js src/status_bar.js src/style.less src/tests/index.html diff --git a/js/webui/src/columns_settings.js b/js/webui/src/columns_settings.js index bc04c4d1..d1be1091 100644 --- a/js/webui/src/columns_settings.js +++ b/js/webui/src/columns_settings.js @@ -1,13 +1,13 @@ import React, { useCallback, useState } from 'react'; import PropTypes from 'prop-types' -import { IconButton, Icon, Select } from './elements.js'; +import { IconButton, Select } from './elements.js'; import ReactModal from 'react-modal'; import { ConfirmDialog, DialogButton } from './dialogs.js'; import { ColumnAlign } from './columns.js'; import { defineKeyedModelData, defineModelData, useColumnsSettingsModel, useDispose, useServices } from './hooks.js'; -import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; -import { SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; +import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; +import { SimpleSortableContext, useDefaultSensors } from './sortable.js'; const AlignItems = [ { id: ColumnAlign.left, name: 'Left' }, @@ -273,36 +273,20 @@ EditableColumn.propTypes = { export function ColumnsSettings() { - const sensors = useSensors( - useSensor(MouseSensor, { - activationConstraint: { - distance: 10, - }, - }), - useSensor(TouchSensor, { - activationConstraint: { - delay: 250, - tolerance: 5, - }, - }), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) - ); - + const sensors = useDefaultSensors(); const model = useColumnsSettingsModel(); const columns = useColumnList(); - const columnElements = columns.map(c => ); + const columnElements = columns.map(c => ); const handleDragEnd = useCallback(e => model.moveColumn(e.active.id, e.over.id), []); useDispose(() => model.applyChanges()); - return - + return ( +
    {columnElements}
    -
    -
    ; + + ); } diff --git a/js/webui/src/playlist_switcher.js b/js/webui/src/playlist_switcher.js index 2ccc2007..be606217 100644 --- a/js/webui/src/playlist_switcher.js +++ b/js/webui/src/playlist_switcher.js @@ -5,9 +5,9 @@ import { Select } from './elements.js'; import urls from './urls.js' import { makeClassName } from './dom_utils.js' import { defineModelData, usePlaylistModel } from './hooks.js'; -import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; -import { SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; +import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; +import { SimpleSortableContext, useDefaultSensors } from './sortable.js'; const usePlaylistsData = defineModelData({ selector(context) @@ -94,22 +94,7 @@ PlaylistTab.propTypes = { export function TabbedPlaylistSwitcher() { - const sensors = useSensors( - useSensor(MouseSensor, { - activationConstraint: { - distance: 10, - }, - }), - useSensor(TouchSensor, { - activationConstraint: { - delay: 250, - tolerance: 5, - }, - }), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) - ); + const sensors = useDefaultSensors(); const model = usePlaylistModel(); const { playlists, activePlaylistId, currentPlaylistId, allowChange } = usePlaylistsData(); @@ -133,11 +118,10 @@ export function TabbedPlaylistSwitcher() isActive={p.id === activePlaylistId}/> )); - return - + return ( +
      {tabs}
    -
    -
    ; + ); } \ No newline at end of file diff --git a/js/webui/src/sortable.js b/js/webui/src/sortable.js new file mode 100644 index 00000000..b7e3be96 --- /dev/null +++ b/js/webui/src/sortable.js @@ -0,0 +1,40 @@ +import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable'; +import React from 'react'; +import PropTypes from 'prop-types'; + +export function useDefaultSensors() +{ + return useSensors( + useSensor(MouseSensor, { + activationConstraint: { + distance: 10, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 5, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); +} + +export function SimpleSortableContext(props) +{ + return + + {props.children} + + ; +} + +SimpleSortableContext.propTypes = { + sensors: PropTypes.object.isRequired, + items: PropTypes.array.isRequired, + onDragEnd: PropTypes.func.isRequired, + disabled: PropTypes.bool, +};