feat: add obs connector

This commit is contained in:
Thibaut Valentin 2025-12-28 21:22:28 +01:00
parent b7a1e58436
commit 448e49b9fd
14 changed files with 1778 additions and 103 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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": {}
}
]
}

View File

@ -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
};

View File

@ -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
}
}
URL.revokeObjectURL(imgUrl);
if (lightBorderCount > darkBorderCount && lightBorderCount >= minPixels) {
cache[src] = darkBackground;
setBackground(darkBackground)
resolve(darkBackground); // Fond sombre
} else if (darkBorderCount > lightBorderCount && darkBorderCount >= minPixels) {
cache[src] = lightBackground;
setBackground(lightBackground)
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)
}
}
setBackground(defaultBackground);
}
};
fetchAndDetect();
}, [src, darkBackground, lightBackground, defaultBackground, tolerance, minPixels, load]);
return <>

View File

@ -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}

View File

@ -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 <OBSContext.Provider value={ret}>
{children}
</OBSContext.Provider>
}
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();
});
}

View File

@ -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() {
<div className="card-body">
<LoadingProvider>
<div className="row">
<CategoryContent cat={cat} catId={catId} setCat={setCat}/>
<CategoryContent cat={cat} catId={catId} setCat={setCat} menuActions={menuActions}/>
</div>
</LoadingProvider>
</div>
<Menu menuActions={menuActions}/>
</div>
</>
}
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(
<>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
style={{color: "#6c757d", cursor: "pointer"}}
onMouseDown={() => 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"))}
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
Launch OBS Modal
</button>
<div className="modal fade" id="OBSModal" tabIndex="-1" aria-labelledby="OBSModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Configuration OBS</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form onSubmit={handleOBSSubmit}>
<div className="modal-body">
<strong>/!\ Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en
changer entre chaque compétition</strong>
<div className="input-group mb-3">
<span className="input-group-text">Adresse du serveur</span>
<span className="input-group-text">ws://</span>
<input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label=""
defaultValue={"127.0.0.1:4455"}/>
<span className="input-group-text">/</span>
</div>
<div className="input-group mb-3">
<span className="input-group-text">Mot de passe du serveur</span>
<input type="password" className="form-control" placeholder="12345" aria-label=""
defaultValue={""}/>
</div>
<div className="input-group mb-3">
<span className="input-group-text">Dossier des resources</span>
<input type="text" className="form-control" placeholder="" aria-label="" required/>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Exporter</button>
</div>
</form>
</div>
</div>
</div>
<div className="modal fade" id="progressModal" tabIndex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Téléchargement en cours...</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="progress">
<div id="progressBar" className="progress-bar" role="progressbar" style={{width: "0%"}}></div>
</div>
<div id="progressText" className="mt-2">Préparation...</div>
</div>
</div>
</div>
</div>
</>
}

View File

@ -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())

View File

@ -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 <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
@ -22,6 +23,7 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
const setLoading = useLoadingSwitcher()
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
const {dispatch} = useWS();
const {connected, setText} = useOBS();
useEffect(() => {
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 <>
<div className="input-group">
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6>

View File

@ -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,7 +28,8 @@ export function CMTable() {
combDispatch({type: 'SET_ALL', payload: {source: "register", data: data}});
}, [data]);
return <PubAffProvider>
return <OBSProvider>
<PubAffProvider>
<div className="text-center">
<div className="row">
<div className="col-md-12 col-lg">
@ -52,8 +57,10 @@ export function CMTable() {
</div>
</div>
<Menu menuActions={menuActions}/>
<ObsAutoSyncWhitPubAff/>
</div>
</PubAffProvider>
</OBSProvider>
}
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(
<>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer"}} onClick={handleSwitchScore}
data-bs-toggle="tooltip2" data-bs-placement="top"
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
onClick={handleSwitchScore} data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title="Inverser la position des combattants sur cette écran"/>
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
style={{color: connected ? "#00c700" : "#6c757d", cursor: "pointer"}}
onMouseDown={() => 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"/>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faDisplay} size="xl"
style={{color: showPubAff ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
@ -134,5 +200,102 @@ function Menu({menuActions}) {
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Afficher les scores sur l'affichage public"/>
</>, document.getElementById("actionMenu"))}
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
Launch OBS Modal
</button>
<div className="modal fade" id="OBSModal" tabIndex="-1" aria-labelledby="OBSModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Configuration OBS</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form onSubmit={handleOBSSubmit}>
<div className="modal-body">
<div className="input-group mb-3">
<span className="input-group-text">Préfix des sources</span>
<span className="input-group-text">sub</span>
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={1} maxLength={1}
defaultValue={localStorage.getItem("obs_prefix") || "1"} required/>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Sauvegarde</button>
</div>
</form>
</div>
</div>
</div>
</>
}
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])
}

View File

@ -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 <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
style={{width: "16px"}} src="/img/171891.png"
alt=""/>
}
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 <>
<div className="col-md-3">
<AddComb groups={groups} setGroups={setGroups} removeGroup={removeGroup}/>
<AddComb groups={groups} setGroups={setGroups} removeGroup={removeGroup} menuActions={menuActions}/>
</div>
<div className="col-md-9">
{cat && <ListMatch cat={cat} matches={matches} groups={groups} reducer={reducer}/>}
@ -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 <>

View File

@ -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;

View File

@ -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 };
}