From 448e49b9fd90d288d6439e4d9d94074f30e00577 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Sun, 28 Dec 2025 21:22:28 +0100 Subject: [PATCH] feat: add obs connector --- src/main/webapp/package-lock.json | 164 +++- src/main/webapp/package.json | 2 + src/main/webapp/public/obs_template.json | 828 ++++++++++++++++++ src/main/webapp/src/assets/SimpleIconsOBS.ts | 26 + .../src/components/SmartLogoBackground.jsx | 116 +-- .../webapp/src/hooks/useExternalWindow.jsx | 13 +- src/main/webapp/src/hooks/useOBS.jsx | 183 ++++ .../src/pages/competition/editor/CMAdmin.jsx | 247 +++++- .../competition/editor/CMTChronoPanel.jsx | 8 +- .../competition/editor/CMTMatchPanel.jsx | 6 + .../src/pages/competition/editor/CMTable.jsx | 229 ++++- .../editor/CategoryAdminContent.jsx | 23 +- .../pages/competition/editor/PubAffWindow.jsx | 8 +- src/main/webapp/src/utils/Tools.js | 28 + 14 files changed, 1778 insertions(+), 103 deletions(-) create mode 100644 src/main/webapp/public/obs_template.json create mode 100644 src/main/webapp/src/assets/SimpleIconsOBS.ts create mode 100644 src/main/webapp/src/hooks/useOBS.jsx diff --git a/src/main/webapp/package-lock.json b/src/main/webapp/package-lock.json index 67a1ffd..c8d23ff 100644 --- a/src/main/webapp/package-lock.json +++ b/src/main/webapp/package-lock.json @@ -18,7 +18,9 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.5", "browser-image-compression": "^2.0.2", + "jszip": "^3.10.1", "leaflet": "^1.9.4", + "obs-websocket-js": "^5.0.7", "proj4": "^2.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -1058,6 +1060,14 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@msgpack/msgpack": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", + "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==", + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1845,6 +1855,11 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -1870,6 +1885,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -2007,7 +2027,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2980,6 +2999,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3018,8 +3042,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.6", @@ -3360,6 +3383,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/iterator.prototype": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", @@ -3447,6 +3478,17 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3474,6 +3516,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3559,8 +3609,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -3696,6 +3745,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obs-websocket-js": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-5.0.7.tgz", + "integrity": "sha512-SdSNSyrLVR6F0ogInKr7qcadV1tYaTUse/vbabxjkUL8hU3P3dyifxkZ7pEkDDrtCp3TkQ53Enx23kgZO0Cjcw==", + "dependencies": { + "@msgpack/msgpack": "^2.7.1", + "crypto-js": "^4.1.1", + "debug": "^4.3.2", + "eventemitter3": "^5.0.1", + "isomorphic-ws": "^5.0.0", + "type-fest": "^3.11.0", + "ws": "^8.13.0" + }, + "engines": { + "node": ">16.0" + } + }, + "node_modules/obs-websocket-js/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "node_modules/obs-websocket-js/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3752,6 +3834,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3855,6 +3942,11 @@ "node": ">=0.8" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/proj4": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.11.0.tgz", @@ -4055,6 +4147,25 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/recharts": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", @@ -4256,6 +4367,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/safe-regex-test": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", @@ -4319,6 +4435,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -4378,6 +4499,14 @@ "node": ">=0.8" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -4723,6 +4852,11 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/uzip": { "version": "0.20201231.0", "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", @@ -4922,6 +5056,26 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 0e57209..418755f 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -20,7 +20,9 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.5", "browser-image-compression": "^2.0.2", + "jszip": "^3.10.1", "leaflet": "^1.9.4", + "obs-websocket-js": "^5.0.7", "proj4": "^2.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/main/webapp/public/obs_template.json b/src/main/webapp/public/obs_template.json new file mode 100644 index 0000000..da724c5 --- /dev/null +++ b/src/main/webapp/public/obs_template.json @@ -0,0 +1,828 @@ +{ + "current_scene": "Ovarlay", + "current_program_scene": "Ovarlay", + "scene_order": [ + { + "name": "Ovarlay" + }, + { + "name": "Scène 2" + } + ], + "name": "saf", + "groups": [], + "quick_transitions": [ + { + "name": "Coupure", + "duration": 300, + "hotkeys": [], + "id": 5, + "fade_to_black": false + }, + { + "name": "Fondu", + "duration": 300, + "hotkeys": [], + "id": 6, + "fade_to_black": false + } + ], + "transitions": [], + "saved_projectors": [], + "canvases": [], + "current_transition": "Fondu", + "transition_duration": 300, + "preview_locked": false, + "scaling_enabled": false, + "scaling_level": -9, + "scaling_off_x": 0.0, + "scaling_off_y": 0.0, + "virtual-camera": { + "type2": 3 + }, + "modules": { + "auto-scene-switcher": { + "interval": 300, + "non_matching_scene": "", + "switch_if_not_matching": false, + "active": false, + "switches": [] + }, + "captions": { + "source": "", + "enabled": false, + "lang_id": 1036, + "provider": "mssapi" + }, + "output-timer": { + "streamTimerHours": 0, + "streamTimerMinutes": 0, + "streamTimerSeconds": 30, + "recordTimerHours": 0, + "recordTimerMinutes": 0, + "recordTimerSeconds": 30, + "autoStartStreamTimer": false, + "autoStartRecordTimer": false, + "pauseRecordTimer": false + }, + "scripts-tool": [] + }, + "version": 1, + "sources": [ + { + "prev_ver": 536870916, + "name": "Capture d'écran", + "uuid": "62b56523-7c2e-4ae2-b4e6-9ca3a36e904b", + "id": "monitor_capture", + "versioned_id": "monitor_capture", + "settings": { + "monitor_id": "\\\\?\\DISPLAY#HWP3320#5&3974db33&0&UID143619#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}" + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "Ovarlay", + "uuid": "670a1c48-dac6-405a-959a-d40662bab4d8", + "id": "scene", + "versioned_id": "scene", + "settings": { + "custom_size": false, + "id_counter": 21, + "items": [ + { + "name": "sub1.img.blue", + "source_uuid": "0a37157d-f356-4657-a2ad-fa4193a298f8", + "visible": true, + "locked": false, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 19, + "group_item_backup": false, + "pos": { + "x": 1789.0, + "y": 950.0 + }, + "scale": { + "x": 0.11296296119689941, + "y": 0.11296296119689941 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.img.rouge", + "source_uuid": "2dfe69bc-9b10-419d-8ee1-6cd9cc53a502", + "visible": true, + "locked": false, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 14, + "group_item_backup": false, + "pos": { + "x": 9.0, + "y": 950.0 + }, + "scale": { + "x": 0.11296296119689941, + "y": 0.11296296119689941 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.comb.blue", + "source_uuid": "4e75e437-20ed-4834-bfac-b668f5cac960", + "visible": true, + "locked": true, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 17, + "group_item_backup": false, + "pos": { + "x": 960.0, + "y": 945.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.comb.rouge", + "source_uuid": "69594be1-dfdc-4eee-893c-fb546e830a6f", + "visible": true, + "locked": true, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 8, + "group_item_backup": false, + "pos": { + "x": 140.0, + "y": 945.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.score.blue", + "source_uuid": "264866ab-58e1-45ec-a32f-c440c32057cd", + "visible": true, + "locked": true, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 20, + "group_item_backup": false, + "pos": { + "x": 1108.0, + "y": 10.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 0.0, + "y": 0.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.score.rouge", + "source_uuid": "8a532b22-7bcd-4742-aa0e-8ea556f39a12", + "visible": true, + "locked": true, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 16, + "group_item_backup": false, + "pos": { + "x": 686.0, + "y": 10.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.temps", + "source_uuid": "a961104f-2cad-4f65-9ff8-d50ff36f8418", + "visible": true, + "locked": true, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 21, + "group_item_backup": false, + "pos": { + "x": 584.0, + "y": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + } + ] + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": { + "OBSBasic.SelectScene": [], + "libobs.show_scene_item.19": [], + "libobs.hide_scene_item.19": [], + "libobs.show_scene_item.14": [], + "libobs.hide_scene_item.14": [], + "libobs.show_scene_item.17": [], + "libobs.hide_scene_item.17": [], + "libobs.show_scene_item.8": [], + "libobs.hide_scene_item.8": [], + "libobs.show_scene_item.20": [], + "libobs.hide_scene_item.20": [], + "libobs.show_scene_item.16": [], + "libobs.hide_scene_item.16": [], + "libobs.show_scene_item.21": [], + "libobs.hide_scene_item.21": [] + }, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e", + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "Scène 2", + "uuid": "d430fe13-0e4a-4d4b-a28e-7c833eb27bf6", + "id": "scene", + "versioned_id": "scene", + "settings": { + "custom_size": false, + "id_counter": 2, + "items": [ + { + "name": "Capture d'écran", + "source_uuid": "62b56523-7c2e-4ae2-b4e6-9ca3a36e904b", + "visible": false, + "locked": false, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 2, + "group_item_backup": false, + "pos": { + "x": 0.0, + "y": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 0.0, + "y": 0.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "Ovarlay", + "source_uuid": "670a1c48-dac6-405a-959a-d40662bab4d8", + "visible": true, + "locked": false, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 1, + "group_item_backup": false, + "pos": { + "x": 0.0, + "y": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 0.0, + "y": 0.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + } + ] + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": { + "OBSBasic.SelectScene": [], + "libobs.show_scene_item.2": [], + "libobs.hide_scene_item.2": [], + "libobs.show_scene_item.1": [], + "libobs.hide_scene_item.1": [] + }, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e", + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.comb.blue", + "uuid": "4e75e437-20ed-4834-bfac-b668f5cac960", + "id": "text_gdiplus", + "versioned_id": "text_gdiplus_v2", + "settings": { + "chatlog": false, + "extents": true, + "gradient": false, + "outline": false, + "undo_sname": "sub1.comb_rouge", + "vertical": false, + "font": { + "face": "Arial", + "flags": 0, + "size": 65, + "style": "Normal" + }, + "align": "right", + "valign": "bottom", + "color": 4294917120, + "extents_cx": 835, + "extents_cy": 120, + "antialiasing": true, + "overlay": true, + "text": "Xavier Login" + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.comb.rouge", + "uuid": "69594be1-dfdc-4eee-893c-fb546e830a6f", + "id": "text_gdiplus", + "versioned_id": "text_gdiplus_v2", + "settings": { + "chatlog": false, + "extents": true, + "gradient": false, + "outline": false, + "undo_sname": "sub1.comb_rouge", + "vertical": false, + "font": { + "face": "Arial", + "flags": 0, + "size": 65, + "style": "Normal" + }, + "align": "left", + "valign": "bottom", + "color": 4278190335, + "extents_cx": 820, + "extents_cy": 120, + "antialiasing": true, + "overlay": true, + "text": "Xavier Login" + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.img.blue", + "uuid": "0a37157d-f356-4657-a2ad-fa4193a298f8", + "id": "slideshow", + "versioned_id": "slideshow", + "settings": { + "files": [], + "transition": "slide", + "slide_time": 9000, + "transition_speed": 1300, + "use_custom_size": "1:1", + "playback_behavior": "stop_restart", + "slide_mode": "mode_auto", + "overlay": true + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": { + "SlideShow.PlayPause": [], + "SlideShow.Restart": [], + "SlideShow.Stop": [], + "SlideShow.NextSlide": [], + "SlideShow.PreviousSlide": [] + }, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.img.rouge", + "uuid": "2dfe69bc-9b10-419d-8ee1-6cd9cc53a502", + "id": "slideshow", + "versioned_id": "slideshow", + "settings": { + "files": [], + "transition": "slide", + "slide_time": 9000, + "transition_speed": 1300, + "use_custom_size": "1:1", + "playback_behavior": "stop_restart", + "slide_mode": "mode_auto", + "overlay": true + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": { + "SlideShow.PlayPause": [], + "SlideShow.Restart": [], + "SlideShow.Stop": [], + "SlideShow.NextSlide": [], + "SlideShow.PreviousSlide": [] + }, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.score.blue", + "uuid": "264866ab-58e1-45ec-a32f-c440c32057cd", + "id": "text_gdiplus", + "versioned_id": "text_gdiplus_v2", + "settings": { + "chatlog": false, + "extents": true, + "gradient": false, + "outline": false, + "text": "0", + "undo_sname": "sub1.comb_rouge", + "vertical": false, + "font": { + "face": "Arial", + "flags": 0, + "size": 80, + "style": "Normal" + }, + "align": "center", + "valign": "bottom", + "color": 4294917120, + "extents_wrap": true, + "extents_cx": 163, + "extents_cy": 80, + "antialiasing": true + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.score.rouge", + "uuid": "8a532b22-7bcd-4742-aa0e-8ea556f39a12", + "id": "text_gdiplus", + "versioned_id": "text_gdiplus_v2", + "settings": { + "chatlog": false, + "extents": true, + "gradient": false, + "outline": false, + "text": "0", + "undo_sname": "sub1.comb_rouge", + "vertical": false, + "font": { + "face": "Arial", + "flags": 0, + "size": 80, + "style": "Normal" + }, + "align": "center", + "valign": "bottom", + "color": 4278190335, + "extents_wrap": true, + "extents_cx": 163, + "extents_cy": 80, + "antialiasing": true + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.temps", + "uuid": "a961104f-2cad-4f65-9ff8-d50ff36f8418", + "id": "text_gdiplus", + "versioned_id": "text_gdiplus_v2", + "settings": { + "extents": true, + "text": "00:10", + "font": { + "face": "Arial", + "flags": 0, + "size": 110, + "style": "Normal" + }, + "align": "center", + "color": -16711936, + "extents_wrap": true, + "extents_cx": 800, + "extents_cy": 110 + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + } + ] +} \ No newline at end of file diff --git a/src/main/webapp/src/assets/SimpleIconsOBS.ts b/src/main/webapp/src/assets/SimpleIconsOBS.ts new file mode 100644 index 0000000..2135033 --- /dev/null +++ b/src/main/webapp/src/assets/SimpleIconsOBS.ts @@ -0,0 +1,26 @@ +import { + IconDefinition, + IconName, + IconPrefix +} from "@fortawesome/fontawesome-svg-core"; + +export const SimpleIconsOBS: IconDefinition = { + icon: [ + // SVG viewbox width (in pixels) + 20, + + // SVG viewbox height (in pixels) + 20, + + // Aliases (not needed) + [], + + // Unicode as hex value (not needed) + "", + + // SVG path data + "M10 0C4.486 0 0 4.486 0 10s4.486 10 10 10 10-4.485 10-10S15.515 0 10 0m8.159 14.075c.55-1.617-.057-3.491-1.551-4.448-1.75-1.12-5.196 1.14-5.196 1.14s-.763 2.815-.026 4.007a5.4 5.4 0 0 1-1.77 1.709c-2.348 1.4-5.372.848-7.054-1.209a9 9 0 0 1-.235-.346c1.139 1.423 3.219 1.831 4.855.908 1.809-1.02 1.427-5.124 1.427-5.124S6.598 8.774 5.26 8.799a5.424 5.424 0 0 1 2.993-7.76c.157-.03.317-.05.476-.072a3.76 3.76 0 1 0 4.525 5.435 5.4 5.4 0 0 1 2.794.793c2.199 1.345 3.119 4.041 2.344 6.397-.072.166-.154.324-.234.483" + ], + iconName: "simple-icons-obs" as IconName, + prefix: "simple-icons" as IconPrefix +}; diff --git a/src/main/webapp/src/components/SmartLogoBackground.jsx b/src/main/webapp/src/components/SmartLogoBackground.jsx index ca5a42a..270a532 100644 --- a/src/main/webapp/src/components/SmartLogoBackground.jsx +++ b/src/main/webapp/src/components/SmartLogoBackground.jsx @@ -2,44 +2,21 @@ import {useEffect, useRef, useState, memo} from 'react'; const cache = {}; -export function SmartLogoBackground({ - src, +export function detectOptimalBackground(blob, darkBackground = '#333333', lightBackground = '#f0f0f0', defaultBackground = 'transparent', tolerance = 55, - minPixels = 10, // Seuil minimal de pixels détectés pour appliquer un fond - alt = 'Logo', - style = {}, - imgClassName = '', - }) { - const canvasRef = useRef(null); - const [background, setBackground] = useState(defaultBackground); - const [load, setLoad] = useState(false) - - useEffect(() => { - if (cache[src]) { - setBackground(cache[src]); - return; - } - if (!load) - return; - - const canvas = canvasRef.current; - const ctx = canvas.getContext('2d'); + minPixels = 10) { + return new Promise((resolve) => { + const imgUrl = URL.createObjectURL(blob); const img = new Image(); - - img.crossOrigin = 'Anonymous'; - img.src = src; - - // Prevent error logging - img.onerror = function () { - return true; - } - + img.crossOrigin = "anonymous"; img.onload = () => { + const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; + const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -69,12 +46,9 @@ export function SmartLogoBackground({ const i = Math.round(y * canvas.width + x) * 4; if (isTransparent(i)) { const neighbors = [ - i - 4, // Haut - i + 4, // Bas - i - 4 * canvas.width, // Gauche - i + 4 * canvas.width, // Droite + i - 4, i + 4, // Haut/Bas + i - 4 * canvas.width, i + 4 * canvas.width, // Gauche/Droite ]; - for (const neighbor of neighbors) { if (neighbor >= 0 && neighbor < pixels.length) { if (isLightColor(neighbor)) lightBorderCount++; @@ -82,24 +56,64 @@ export function SmartLogoBackground({ } } } - - - if (lightBorderCount >= 25 || darkBorderCount >= 25) - break - } - - if (lightBorderCount > darkBorderCount && lightBorderCount >= minPixels) { - cache[src] = darkBackground; - setBackground(darkBackground) - } else if (darkBorderCount > lightBorderCount && darkBorderCount >= minPixels) { - cache[src] = lightBackground; - setBackground(lightBackground) - } else { - cache[src] = defaultBackground; - setBackground(defaultBackground) } } - } + + URL.revokeObjectURL(imgUrl); + if (lightBorderCount > darkBorderCount && lightBorderCount >= minPixels) { + resolve(darkBackground); // Fond sombre + } else if (darkBorderCount > lightBorderCount && darkBorderCount >= minPixels) { + resolve(lightBackground); // Fond clair + } else { + resolve(defaultBackground); // Fond transparent + } + }; + img.onerror = () => { + URL.revokeObjectURL(imgUrl); + resolve(defaultBackground); // En cas d'erreur + }; + img.src = imgUrl; + }); +} + +export function SmartLogoBackground({ + src, + darkBackground = '#333333', + lightBackground = '#f0f0f0', + defaultBackground = 'transparent', + tolerance = 55, + minPixels = 10, // Seuil minimal de pixels détectés pour appliquer un fond + alt = 'Logo', + style = {}, + imgClassName = '', + }) { + const canvasRef = useRef(null); + const [background, setBackground] = useState(defaultBackground); + const [load, setLoad] = useState(false) + + useEffect(() => { + if (!load) return; + + const fetchAndDetect = async () => { + if (cache[src]) { + setBackground(cache[src]); + return; + } + + try { + const response = await fetch(src); + const blob = await response.blob(); + const detectedBackground = await detectOptimalBackground(blob, darkBackground, lightBackground, defaultBackground, tolerance, minPixels); + cache[src] = detectedBackground; + setBackground(detectedBackground); + } catch (error) { + console.error("Erreur de détection du fond:", error); + cache[src] = defaultBackground; + setBackground(defaultBackground); + } + }; + + fetchAndDetect(); }, [src, darkBackground, lightBackground, defaultBackground, tolerance, minPixels, load]); return <> diff --git a/src/main/webapp/src/hooks/useExternalWindow.jsx b/src/main/webapp/src/hooks/useExternalWindow.jsx index 233b052..db7f53d 100644 --- a/src/main/webapp/src/hooks/useExternalWindow.jsx +++ b/src/main/webapp/src/hooks/useExternalWindow.jsx @@ -1,6 +1,15 @@ import {createContext, useContext, useReducer} from "react"; -const PubAffContext = createContext({next: [], c1: undefined, c2: undefined, showScore: true, timeCb: undefined, scoreRouge: 0, scoreBleu: 0}); +const PubAffContext = createContext({ + next: [], + c1: undefined, + c2: undefined, + showScore: true, + timeCb: undefined, + timeCb2: undefined, + scoreRouge: 0, + scoreBleu: 0 +}); const PubAffDispatchContext = createContext(() => { }); @@ -11,6 +20,8 @@ function reducer(state, action) { case 'CALL_TIME': if (state.timeCb) state.timeCb(action.payload) + if (state.timeCb2) + state.timeCb2(action.payload) return state case 'CLEAR_CB_TIME': return {...state, timeCb: undefined} diff --git a/src/main/webapp/src/hooks/useOBS.jsx b/src/main/webapp/src/hooks/useOBS.jsx new file mode 100644 index 0000000..6f74831 --- /dev/null +++ b/src/main/webapp/src/hooks/useOBS.jsx @@ -0,0 +1,183 @@ +import {createContext, useContext, useEffect, useRef, useState} from "react"; +import OBSWebSocket from "obs-websocket-js"; +import {hex2rgb} from "../utils/Tools.js"; + +const OBSContext = createContext({connected: false, obs: null}) + +export function OBSProvider({children}) { + const obs = useRef(null) + const obsParm = useRef(null) + const assets = useRef(null) + const [connected, setConnected] = useState(false) + const [doReconnect, setDoReconnect] = useState(false) + + useEffect(() => { + if (!doReconnect) + return; + + const timer = setInterval(() => { + if (obs.current && !connected && doReconnect) { + console.log("Reconnecting to OBS WebSocket...") + obs.current.connect(obsParm.current.adresse, obsParm.current.password) + .then(data => { + console.log("Reconnected to OBS WebSocket", data) + setDoReconnect(false) + }) + } + }, 5000); + return () => clearInterval(timer); + }, [connected, doReconnect]); + + const connect = (adresse, password = undefined, assets_dir = undefined) => { + if (connected && obs.current) + return; + assets.current = assets_dir; + + const obs_ = new OBSWebSocket(); + obs_.connect(adresse, password) + .then(data => { + console.log("Connected to OBS WebSocket", data) + }) + .catch(err => { + console.error("Failed to connect to OBS WebSocket", err) + }); + obs_.on('ConnectionOpened', () => { + setConnected(true) + console.log("OBS WebSocket connection opened") + }); + obs_.on('ConnectionClosed', err => { + setConnected(false) + console.log("OBS WebSocket connection closed", err.code, err.message) + if (err.code === 1000 || err.code === 4009) // 1000 = Normal Closure, 4009 = Authentication Failure + return; + obsParm.current = {adresse, password} + setDoReconnect(true) + }); + obs_.on('error', err => { + console.error("OBS WebSocket error", err) + }); + + obs.current = obs_; + } + + const disconnect = () => { + if (obs.current && connected) { + obs.current.disconnect(); + obs.current = null; + } + } + + const ret = {connected, obs: obs.current, connect, disconnect, assets: assets.current} + return + {children} + +} + +function getElementName(element) { + return `sub${sessionStorage.getItem("obs_prefix") || 1}.${element}` +} + +export function useOBS() { + const {connected, obs, connect, disconnect, assets} = useContext(OBSContext) + const setTextAndColor = (element, text, color) => { + if (!connected) + return; + if (color.startsWith("#")){ + const tmp = hex2rgb(color); + color = (tmp.b << 16) + (tmp.g << 8) + tmp.r; + } + + obs.call('SetInputSettings', { + inputName: getElementName(element), + inputSettings: { + text: text, + color: color, + overlay: true, + } + }).catch(err => { + console.error("Failed to set text and color for OBS element", element, err) + }); + } + const setText = (element, text) => { + if (!connected) + return; + obs.call('SetInputSettings', { + inputName: getElementName(element), + inputSettings: { + text: text, + overlay: true, + } + }).catch(err => { + console.error("Failed to set text for OBS element", element, err) + }); + } + + const setDiapo = (element, files) => { + if (!connected) + return; + const arr = []; + for (const file of files) { + arr.push({ + hidden: false, + selected: false, + value: (assets.endsWith('/') ? assets : assets + '/') + file, + }) + } + obs.call('SetInputSettings', { + inputName: getElementName(element), + inputSettings: { + files: arr, + overlay: true, + } + }).catch(err => { + console.error("Failed to set diapo for OBS element", element, err) + }); + } + + return { + connected, + obs, + connect, + disconnect, + setText, + setTextAndColor, + setDiapo, + } +} + +export function exportOBSConfiguration(adresse, password, assets_dir) { + const config = { + adresse: adresse, + password: password, + assets_dir: assets_dir, + }; + const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(config, null, 2)); + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href", dataStr); + downloadAnchorNode.setAttribute("download", "obs_configuration.json"); + document.body.appendChild(downloadAnchorNode); // required for firefox + downloadAnchorNode.click(); + downloadAnchorNode.remove(); +} + +export async function importOBSConfiguration() { + return new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'application/json'; + input.onchange = e => { + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = event => { + try { + const config = JSON.parse(event.target.result); + resolve(config); + } catch (err) { + reject(err); + } + }; + reader.readAsText(file); + }; + input.click(); + }); +} diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index 6bc3372..a844107 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -1,14 +1,21 @@ -import {useEffect, useRef, useState} from "react"; +import React, {useEffect, useRef, useState} from "react"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {toast} from "react-toastify"; import {build_tree, resize_tree} from "../../../utils/TreeUtils.js" import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; import {CategoryContent} from "./CategoryAdminContent.jsx"; +import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx"; +import {createPortal} from "react-dom"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; +import JSZip from "jszip"; +import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx"; export function CMAdmin() { const [catId, setCatId] = useState(null); const [cat, setCat] = useState(null); + const menuActions = useRef({}); const {dispatch} = useWS(); useEffect(() => { @@ -35,10 +42,246 @@ export function CMAdmin() {
- +
+ + + +} + +let tto = []; + +function resizeImageWithOptimalBackground(blob) { + return new Promise(async (resolve) => { + const background = await detectOptimalBackground(blob); + const imgUrl = URL.createObjectURL(blob); + const img = new Image(); + img.crossOrigin = "anonymous"; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = 1080; + canvas.height = 1080; + const ctx = canvas.getContext('2d'); + + // Dessiner l'image centrée + const scale = Math.min(1080 / img.width, 1080 / img.height); + const newWidth = img.width * scale; + const newHeight = img.height * scale; + const x = (1080 - newWidth) / 2; + const y = (1080 - newHeight) / 2; + + ctx.filter = `drop-shadow(0 0 2rem ${background})`; + ctx.drawImage(img, x, y, newWidth, newHeight); + + // Exporter en PNG + canvas.toBlob((newBlob) => { + resolve(newBlob); + URL.revokeObjectURL(imgUrl); + }, 'image/png', 1.0); + }; + img.onerror = () => resolve(blob); // Retourne l'original en cas d'erreur + img.src = imgUrl; + }); +} + +async function downloadResourcesAsZip(resourceList) { + const zip = new JSZip(); + const modal = new bootstrap.Modal(document.getElementById('progressModal')); + const progressBar = document.getElementById('progressBar'); + const progressText = document.getElementById('progressText'); + let completed = 0; + + if (!resourceList.some(d => d.url === '/obs_template.json')) + resourceList.push({url: '/obs_template.json', name: 'saf_obs_template.json'}); + + // Afficher la modale + modal.show(); + + // Fonction pour télécharger une ressource et l'ajouter au ZIP + const addResourceToZip = async (data) => { + try { + const response = await fetch(data.url); + if (!response.ok) { + if (response.status === 404) { + return {success: false, filename: data.name || data.url.split('/').pop()}; + } + // noinspection ExceptionCaughtLocallyJS + throw new Error(`Erreur HTTP: ${response.status}`); + } + const blob = await response.blob(); + const filename = data.name || data.url.split('/').pop(); + const format = filename.split('.').pop().toLowerCase(); + + if (['png', 'jpg', 'jpeg', 'svg'].includes(format)) { + const resizedBlob = await resizeImageWithOptimalBackground(blob); + const pngFilename = filename.replace(/\.[^/.]+$/, ".png"); + zip.file(pngFilename, resizedBlob); + return {success: true, pngFilename}; + } else { + zip.file(filename, blob); + return {success: true, filename}; + } + } catch (error) { + console.error(`Impossible d'ajouter ${data.url} au ZIP:`, error); + return {success: false, filename: data.name || data.url.split('/').pop()}; + } + }; + + // Télécharger toutes les ressources et mettre à jour la progression + await Promise.all( + resourceList.map(async (data) => { + const result = await addResourceToZip(data); + completed++; + const progress = Math.round((completed / resourceList.length) * 100); + progressBar.style.width = `${progress}%`; + progressText.textContent = `Téléchargement (${completed}/${resourceList.length}) : ${result.filename}`; + return result; + }) + ); + + // Générer le ZIP et déclencher le téléchargement + const zipBlob = await zip.generateAsync({type: 'blob'}); + const zipUrl = URL.createObjectURL(zipBlob); + const a = document.createElement('a'); + a.href = zipUrl; + a.download = 'ressources.zip'; + a.click(); + URL.revokeObjectURL(zipUrl); + + // Fermer la modale + modal.hide(); + progressText.textContent = "Téléchargement terminé !"; +} + +function Menu({menuActions}) { + const e = document.getElementById("actionMenu") + const longPress = useRef({time: null, timer: null, button: null}); + const obsModal = useRef(null); + + for (const x of tto) + x.dispose(); + const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]') + tto = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) + + const longTimeAction = (button) => { + if (button === "obs") { + obsModal.current.click(); + } + } + + const longPressDown = (button) => { + longPress.current.button = button; + longPress.current.time = new Date(); + longPress.current.timer = setTimeout(() => { + longTimeAction(button); + + longPress.current.time = null; + longPress.current.button = null; + }, 1000); + } + + const longPressUp = (button) => { + clearTimeout(longPress.current.timer); + + if (longPress.current.time) { + const diff = new Date() - longPress.current.time; + if (longPress.current.button === button) { + if (diff >= 1000) { + longTimeAction(button); + } else { + if (button === "obs") { + downloadResourcesAsZip(menuActions.current.resourceList || []) + .then(__ => console.log("Ressources téléchargées")); + } + } + } + + longPress.current.time = null; + longPress.current.button = null; + } + } + + const handleOBSSubmit = (e) => { + e.preventDefault(); + const form = e.target; + const adresse = form[0].value; + const password = form[1].value; + const assets_dir = form[2].value; + + exportOBSConfiguration(adresse, password, assets_dir) + } + + if (!e) + return <>; + return <> + {createPortal( + <> +
+ longPressDown("obs")} + onMouseUp={() => longPressUp("obs")} + data-bs-toggle="tooltip2" data-bs-placement="top" + data-bs-title="Clique court : Télécharger les ressources. Clique long : Créer la configuration obs"/> + , document.getElementById("actionMenu"))} + + + + + } diff --git a/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx index 40ac209..2c744b5 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx @@ -9,7 +9,7 @@ export function ChronoPanel() { }) const [chrono, setChrono] = useState({time: 0, startTime: 0}) const chronoText = useRef(null) - const state = useRef({chronoState: 0, countBlink: 20, lastColor: "black", lastTimeStr: "00:00"}) + const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"}) const publicAffDispatch = usePubAffDispatch(); const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time})) @@ -34,12 +34,12 @@ export function ChronoPanel() { const timer = setInterval(() => { let currentDuration = config.time - let color = "black" + let color = "#000000" if (state_.chronoState === 1) { - color = (state_.countBlink < blinkRfDuration) ? "black" : "red" + color = (state_.countBlink < blinkRfDuration) ? "#000000" : "#ff0000" } else if (state_.chronoState === 2) { currentDuration = (state_.chronoState === 0) ? 10000 : config.pause - color = (state_.countBlink < blinkRfDuration) ? "green" : "red" + color = (state_.countBlink < blinkRfDuration) ? "#008000" : "#ff0000" } const timeStr = timePrint(currentDuration - getTime()) diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index d6f2966..a4522be 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -11,6 +11,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons"; import {toast} from "react-toastify"; import "./CMTMatchPanel.css" +import {useOBS} from "../../../hooks/useOBS.jsx"; function CupImg() { return { const categoryListener = ({data}) => { @@ -47,6 +49,10 @@ export function CategorieSelect({catId, setCatId, menuActions}) { const cat = cats?.find(c => c.id === catId); + useEffect(() => { + setText("poule",cat ? cat.name : ""); + }, [cat, connected]); + return <>
Catégorie
diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx index 1828795..287b77e 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -1,16 +1,20 @@ import React, {useEffect, useRef, useState} from "react"; import {useRequestWS} from "../../../hooks/useWS.jsx"; -import {useCombsDispatch} from "../../../hooks/useComb.jsx"; +import {useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {createPortal} from "react-dom"; import {copyStyles} from "../../../utils/copyStyles.js"; -import {PubAffProvider, usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx"; +import {PubAffProvider, usePubAffDispatch, usePubAffState} from "../../../hooks/useExternalWindow.jsx"; import {faArrowRightArrowLeft, faDisplay} from "@fortawesome/free-solid-svg-icons"; import {PubAffWindow} from "./PubAffWindow.jsx"; import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts"; import {ChronoPanel} from "./CMTChronoPanel.jsx"; import {CategorieSelect} from "./CMTMatchPanel.jsx"; import {PointPanel} from "./CMTPoint.jsx"; +import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx"; +import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; +import {timePrint} from "../../../utils/Tools.js"; +import {toast} from "react-toastify"; export function CMTable() { const combDispatch = useCombsDispatch() @@ -24,36 +28,39 @@ export function CMTable() { combDispatch({type: 'SET_ALL', payload: {source: "register", data: data}}); }, [data]); - return -
-
-
-
-
Chronomètre
-
- + return + +
+
+
+
+
Chronomètre
+
+ +
-
-
-
Score
-
- -
-
-
-
-
-
Matches
-
- +
+
Score
+
+ +
+
+
+
+
+
Matches
+
+ +
+ +
- -
-
+ +
} const windowName = "FFSAFScorePublicWindow"; @@ -65,6 +72,9 @@ function Menu({menuActions}) { const publicAffDispatch = usePubAffDispatch() const [showPubAff, setShowPubAff] = useState(false) const [showScore, setShowScore] = useState(true) + const {connected, connect, disconnect} = useOBS(); + const longPress = useRef({time: null, timer: null, button: null}); + const obsModal = useRef(null); const externalWindow = useRef(null) const containerEl = useRef(document.createElement("div")) @@ -73,11 +83,6 @@ function Menu({menuActions}) { if (sessionStorage.getItem(windowName + "_open") === "true") { handlePubAff(); } - //return () => { - // if (!externalWindow.current) - // return; - // externalWindow.current.close(); - //} }, []); const handlePubAff = __ => { @@ -115,15 +120,76 @@ function Menu({menuActions}) { menuActions.current.switchSore?.(); } + const longTimeAction = (button) => { + if (button === "obs") { + obsModal.current.click(); + } + } + + const longPressDown = (button) => { + longPress.current.button = button; + longPress.current.time = new Date(); + longPress.current.timer = setTimeout(() => { + longTimeAction(button); + + longPress.current.time = null; + longPress.current.button = null; + }, 1000); + } + + const longPressUp = (button) => { + clearTimeout(longPress.current.timer); + + if (longPress.current.time) { + const diff = new Date() - longPress.current.time; + if (longPress.current.button === button) { + if (diff >= 1000) { + longTimeAction(button); + } else { + if (button === "obs") { + if (connected) { + disconnect(); + } else { + importOBSConfiguration() + .then(config => { + connect("ws://" + config.adresse + "/", config.password, config.assets_dir); + }) + .catch(() => { + toast.error("Aucune configuration OBS trouvée, veuillez en importer une"); + }); + } + } + } + } + + longPress.current.time = null; + longPress.current.button = null; + } + } + + const handleOBSSubmit = (e) => { + e.preventDefault(); + const form = e.target; + const prefix = form[0].value; + + sessionStorage.setItem("obs_prefix", prefix); + } + if (!e) return <>; return <> {createPortal( <>
- + longPressDown("obs")} + onMouseUp={() => longPressUp("obs")} + data-bs-toggle="tooltip2" data-bs-placement="top" + data-bs-title="Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice"/>
, document.getElementById("actionMenu"))} {externalWindow.current && createPortal(, containerEl.current)} + + + } + +function ObsAutoSyncWhitPubAff() { + const {connected, setText, setTextAndColor, setDiapo} = useOBS(); + const oldState = useRef({timeColor: "#000000", timeStr: "--:--", c1: null, c2: null, showScore: true, scoreRouge: 0, scoreBleu: 0}); + const state = usePubAffState(); + const {getComb} = useCombs(); + + useEffect(() => { + if (state.c1 !== oldState.current.c1) { + const comb = getComb(state.c1); + setText("comb.rouge", comb ? (comb?.fname + " " + comb?.lname) : ""); + const files = [] + if (comb?.club_uuid) files.push(`club_${comb.club_uuid}.png`) + if (comb?.country) files.push(`flag_${comb.country.toLowerCase()}.png`) + setDiapo("img.rouge", files); + oldState.current.c1 = state.c1; + } + + if (state.c2 !== oldState.current.c2) { + const comb = getComb(state.c2); + setText("comb.blue", comb ? (comb?.fname + " " + comb?.lname) : ""); + const files = [] + if (comb?.club_uuid) files.push(`club_${comb.club_uuid}.png`) + if (comb?.country) files.push(`flag_${comb.country.toLowerCase()}.png`) + setDiapo("img.blue", files); + oldState.current.c2 = state.c2; + } + + if (state.showScore !== oldState.current.showScore) { + setText("score.rouge", state.showScore ? state.scoreRouge.toString() : ""); + setText("score.blue", state.showScore ? state.scoreBleu.toString() : ""); + oldState.current.showScore = state.showScore; + } + + if (state.showScore === undefined || state.showScore) { + if (state.scoreRouge !== oldState.current.scoreRouge) { + setText("score.rouge", (state.scoreRouge || 0).toString()); + oldState.current.scoreRouge = state.scoreRouge; + } + if (state.scoreBleu !== oldState.current.scoreBleu) { + setText("score.blue", (state.scoreBleu || 0).toString()); + oldState.current.scoreBleu = state.scoreBleu; + } + } + }, [state]); + + state.timeCb2 = (payload) => { + if (payload.timeStr && payload.timeColor) { + setTextAndColor("temps", payload.timeStr, payload.timeColor === "#000000" ? "#ffffff" : payload.timeColor); + + oldState.current.timeStr = payload.timeStr; + oldState.current.lastColor = payload.timeColor; + } + } + + useEffect(() => { + if (!connected) + return; + // Initial sync + const comb = getComb(oldState.current.c1); + const comb2 = getComb(oldState.current.c2); + setText("comb.rouge", comb ? (comb?.fname + " " + comb?.lname) : ""); + setText("comb.blue", comb2 ? (comb2?.fname + " " + comb2?.lname) : ""); + setTextAndColor("temps", oldState.current.timeStr, oldState.current.timeColor === "#000000" ? "#ffffff" : oldState.current.timeColor); + setText("score.rouge", oldState.current.showScore === undefined || oldState.current.showScore ? oldState.current.scoreRouge.toString() : ""); + setText("score.blue", oldState.current.showScore === undefined || oldState.current.showScore ? oldState.current.scoreBleu.toString() : ""); + + }, [connected]) +} diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index 73d349f..faae064 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -17,13 +17,15 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faTrash} from "@fortawesome/free-solid-svg-icons"; import {win} from "../../../utils/Tools.js"; +const vite_url = import.meta.env.VITE_URL; + function CupImg() { return } -export function CategoryContent({cat, catId, setCat}) { +export function CategoryContent({cat, catId, setCat, menuActions}) { const setLoading = useLoadingSwitcher() const {sendRequest, dispatch} = useWS(); const [matches, reducer] = useReducer(MarchReducer, []); @@ -148,7 +150,7 @@ export function CategoryContent({cat, catId, setCat}) { return <>
- +
{cat && } @@ -156,7 +158,7 @@ export function CategoryContent({cat, catId, setCat}) { } -function AddComb({groups, setGroups, removeGroup}) { +function AddComb({groups, setGroups, removeGroup, menuActions}) { const {data, setData} = useRequestWS("getRegister", null) const combDispatch = useCombsDispatch() const {dispatch} = useWS() @@ -190,6 +192,21 @@ function AddComb({groups, setGroups, removeGroup}) { if (data === null) return; combDispatch({type: 'SET_ALL', payload: {source: "register", data: data}}); + + const resourceList = [] + data.forEach(d => { + if (d.club_uuid) { + const url = `${vite_url}/api/club/${d.club_uuid}/logo`; + if (!resourceList.some(d => d.url === url)) + resourceList.push({url: url, name: `club_${d.club_uuid}.png`}); + } + if (d.country) { + const url = `/flags/svg/${d.country.toLowerCase()}.svg`; + if (!resourceList.some(d => d.url === url)) + resourceList.push({url: url, name: `flag_${d.country.toLowerCase()}.svg`}); + } + }) + menuActions.current.resourceList = resourceList }, [data]); return <> diff --git a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx index 8eac0bc..0b7273d 100644 --- a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx +++ b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx @@ -14,16 +14,16 @@ const text2Style = {fontSize: "min(1.7vw, 7vh)", fontWeight: "bold"}; export function PubAffWindow({document}) { const chronoText = useRef(null) - const state2 = useRef({lastColor: "white", lastTimeStr: "--:--"}) + const state2 = useRef({lastColor: "#ffffff", lastTimeStr: "--:--"}) const state = usePubAffState(); - document.title = "A React portal window" + document.title = "Affichage Public"; document.body.className = "bg-dark text-white overflow-hidden"; state.timeCb = (payload) => { - state2.current = {lastColor: payload.timeColor === "black" ? "white" : payload.timeColor, lastTimeStr: payload.timeStr} + state2.current = {lastColor: payload.timeColor === "#000000" ? "#ffffff" : payload.timeColor, lastTimeStr: payload.timeStr} chronoText.current.textContent = payload.timeStr - chronoText.current.style.color = payload.timeColor === "black" ? "white" : payload.timeColor + chronoText.current.style.color = payload.timeColor === "#000000" ? "#ffffff" : payload.timeColor } const showScore = state.showScore ?? true; diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index 5821030..a74a8e3 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -153,3 +153,31 @@ export function timePrint(time, negSign = false) { String(min).padStart(2, '0') + ":" + String(sec).padStart(2, '0') } + +//create full hex +function fullHex (hex) { + let r = hex.slice(1,2); + let g = hex.slice(2,3); + let b = hex.slice(3,4); + + r = parseInt(r+r, 16); + g = parseInt(g+g, 16); + b = parseInt(b+b, 16); + + // return {r, g, b} + return { r, g, b }; +} + +//convert hex to rgb +export function hex2rgb (hex) { + if(hex.length === 4){ + return fullHex(hex); + } + + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + + // return {r, g, b} + return { r, g, b }; +}