mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Add prettier
This commit is contained in:
13
.prettierrc
Normal file
13
.prettierrc
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"printWidth":120,
|
||||||
|
"tabWidth":2,
|
||||||
|
"useTabs":false,
|
||||||
|
"semi":true,
|
||||||
|
"singleQuote":false,
|
||||||
|
"trailingComma":"all",
|
||||||
|
"bracketSpacing":true,
|
||||||
|
"arrowParens":"always",
|
||||||
|
"requirePragma":false,
|
||||||
|
"insertPragma":false,
|
||||||
|
"proseWrap":"always"
|
||||||
|
}
|
||||||
79
package-lock.json
generated
79
package-lock.json
generated
@@ -9,7 +9,8 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^35.7.5",
|
"electron": "^35.7.5",
|
||||||
"electron-builder": "^24.13.3"
|
"electron-builder": "^24.13.3",
|
||||||
|
"prettier": "^3.7.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@develar/schema-utils": {
|
"node_modules/@develar/schema-utils": {
|
||||||
@@ -675,7 +676,6 @@
|
|||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -830,6 +830,7 @@
|
|||||||
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^2.1.0",
|
"archiver-utils": "^2.1.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
@@ -849,6 +850,7 @@
|
|||||||
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.1.4",
|
"glob": "^7.1.4",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@@ -871,6 +873,7 @@
|
|||||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@@ -886,7 +889,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -894,6 +898,7 @@
|
|||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@@ -995,6 +1000,7 @@
|
|||||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
"inherits": "^2.0.4",
|
"inherits": "^2.0.4",
|
||||||
@@ -1367,6 +1373,7 @@
|
|||||||
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-crc32": "^0.2.13",
|
"buffer-crc32": "^0.2.13",
|
||||||
"crc32-stream": "^4.0.2",
|
"crc32-stream": "^4.0.2",
|
||||||
@@ -1466,6 +1473,7 @@
|
|||||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"crc32": "bin/crc32.njs"
|
"crc32": "bin/crc32.njs"
|
||||||
},
|
},
|
||||||
@@ -1479,6 +1487,7 @@
|
|||||||
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crc-32": "^1.2.0",
|
"crc-32": "^1.2.0",
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
@@ -1656,7 +1665,6 @@
|
|||||||
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
|
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"builder-util": "24.13.1",
|
"builder-util": "24.13.1",
|
||||||
@@ -1841,6 +1849,7 @@
|
|||||||
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
|
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
@@ -1854,6 +1863,7 @@
|
|||||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^6.0.1",
|
"jsonfile": "^6.0.1",
|
||||||
@@ -1869,6 +1879,7 @@
|
|||||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -1882,6 +1893,7 @@
|
|||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
@@ -2198,7 +2210,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
@@ -2694,7 +2707,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/isbinaryfile": {
|
"node_modules/isbinaryfile": {
|
||||||
"version": "5.0.7",
|
"version": "5.0.7",
|
||||||
@@ -2831,6 +2845,7 @@
|
|||||||
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readable-stream": "^2.0.5"
|
"readable-stream": "^2.0.5"
|
||||||
},
|
},
|
||||||
@@ -2844,6 +2859,7 @@
|
|||||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@@ -2859,7 +2875,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lazystream/node_modules/string_decoder": {
|
"node_modules/lazystream/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -2867,6 +2884,7 @@
|
|||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@@ -2883,35 +2901,40 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.difference": {
|
"node_modules/lodash.difference": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.flatten": {
|
"node_modules/lodash.flatten": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.isplainobject": {
|
"node_modules/lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.union": {
|
"node_modules/lodash.union": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lowercase-keys": {
|
"node_modules/lowercase-keys": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -3100,6 +3123,7 @@
|
|||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3228,12 +3252,29 @@
|
|||||||
"node": ">=10.4.0"
|
"node": ">=10.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||||
|
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/progress": {
|
"node_modules/progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
@@ -3317,6 +3358,7 @@
|
|||||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
"string_decoder": "^1.1.1",
|
"string_decoder": "^1.1.1",
|
||||||
@@ -3332,6 +3374,7 @@
|
|||||||
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimatch": "^5.1.0"
|
"minimatch": "^5.1.0"
|
||||||
}
|
}
|
||||||
@@ -3414,7 +3457,8 @@
|
|||||||
"url": "https://feross.org/support"
|
"url": "https://feross.org/support"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@@ -3610,6 +3654,7 @@
|
|||||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
@@ -3722,6 +3767,7 @@
|
|||||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"bl": "^4.0.3",
|
||||||
"end-of-stream": "^1.4.1",
|
"end-of-stream": "^1.4.1",
|
||||||
@@ -3879,7 +3925,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/verror": {
|
"node_modules/verror": {
|
||||||
"version": "1.10.1",
|
"version": "1.10.1",
|
||||||
@@ -4030,6 +4077,7 @@
|
|||||||
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^3.0.4",
|
"archiver-utils": "^3.0.4",
|
||||||
"compress-commons": "^4.1.2",
|
"compress-commons": "^4.1.2",
|
||||||
@@ -4045,6 +4093,7 @@
|
|||||||
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.2.3",
|
"glob": "^7.2.3",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -6,16 +6,24 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"appId": "dev.kruhlmann.imgfloat",
|
"appId": "dev.kruhlmann.imgfloat",
|
||||||
"productName": "Imgfloat",
|
"productName": "Imgfloat",
|
||||||
"files": [ "src/main/node/app.js" ],
|
"files": [
|
||||||
|
"src/main/node/app.js"
|
||||||
|
],
|
||||||
"asar": false,
|
"asar": false,
|
||||||
"directories": { "output": "dist" },
|
"directories": {
|
||||||
|
"output": "dist"
|
||||||
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": ["AppImage"],
|
"target": [
|
||||||
|
"AppImage"
|
||||||
|
],
|
||||||
"category": "Utility",
|
"category": "Utility",
|
||||||
"icon": "src/main/resources/assets/icon/raw_icon.png"
|
"icon": "src/main/resources/assets/icon/raw_icon.png"
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
"target": ["nsis"],
|
"target": [
|
||||||
|
"nsis"
|
||||||
|
],
|
||||||
"icon": "src/main/resources/assets/icon/appicon.ico"
|
"icon": "src/main/resources/assets/icon/appicon.ico"
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
@@ -34,6 +42,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^35.7.5",
|
"electron": "^35.7.5",
|
||||||
"electron-builder": "^24.13.3"
|
"electron-builder": "^24.13.3",
|
||||||
|
"prettier": "^3.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { app, BrowserWindow } = require('electron');
|
const { app, BrowserWindow } = require("electron");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const url = "https://imgfloat.kruhlmann.dev/channels";
|
const url = "https://imgfloat.kruhlmann.dev/channels";
|
||||||
@@ -10,7 +10,7 @@ function createWindow() {
|
|||||||
height: initialWindowHeightPx,
|
height: initialWindowHeightPx,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
frame: true,
|
frame: true,
|
||||||
backgroundColor: '#00000000',
|
backgroundColor: "#00000000",
|
||||||
alwaysOnTop: false,
|
alwaysOnTop: false,
|
||||||
icon: path.join(__dirname, "../resources/assets/icon/appicon.ico"),
|
icon: path.join(__dirname, "../resources/assets/icon/appicon.ico"),
|
||||||
webPreferences: { backgroundThrottling: false },
|
webPreferences: { backgroundThrottling: false },
|
||||||
@@ -77,13 +77,13 @@ function createWindow() {
|
|||||||
|
|
||||||
applicationWindow.loadURL(url);
|
applicationWindow.loadURL(url);
|
||||||
|
|
||||||
applicationWindow.webContents.on('did-finish-load', () => {
|
applicationWindow.webContents.on("did-finish-load", () => {
|
||||||
handleNavigation(applicationWindow.webContents.getURL());
|
handleNavigation(applicationWindow.webContents.getURL());
|
||||||
});
|
});
|
||||||
|
|
||||||
applicationWindow.webContents.on('did-navigate', (_event, navigationUrl) => handleNavigation(navigationUrl));
|
applicationWindow.webContents.on("did-navigate", (_event, navigationUrl) => handleNavigation(navigationUrl));
|
||||||
applicationWindow.webContents.on('did-navigate-in-page', (_event, navigationUrl) => handleNavigation(navigationUrl));
|
applicationWindow.webContents.on("did-navigate-in-page", (_event, navigationUrl) => handleNavigation(navigationUrl));
|
||||||
applicationWindow.on('closed', clearCanvasSizeInterval);
|
applicationWindow.on("closed", clearCanvasSizeInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(createWindow);
|
app.whenReady().then(createWindow);
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ body {
|
|||||||
|
|
||||||
.landing-body {
|
.landing-body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: radial-gradient(circle at 10% 20%, rgba(124, 58, 237, 0.18), transparent 30%),
|
background:
|
||||||
radial-gradient(circle at 80% 0%, rgba(59, 130, 246, 0.16), transparent 25%),
|
radial-gradient(circle at 10% 20%, rgba(124, 58, 237, 0.18), transparent 30%),
|
||||||
#0f172a;
|
radial-gradient(circle at 80% 0%, rgba(59, 130, 246, 0.16), transparent 25%), #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing {
|
.landing {
|
||||||
@@ -59,25 +59,28 @@ body {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channels-body, .settings-body {
|
.channels-body,
|
||||||
|
.settings-body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: radial-gradient(circle at 10% 20%, rgba(124, 58, 237, 0.16), transparent 30%),
|
background:
|
||||||
radial-gradient(circle at 85% 10%, rgba(59, 130, 246, 0.18), transparent 28%),
|
radial-gradient(circle at 10% 20%, rgba(124, 58, 237, 0.16), transparent 30%),
|
||||||
#0f172a;
|
radial-gradient(circle at 85% 10%, rgba(59, 130, 246, 0.18), transparent 28%), #0f172a;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: clamp(24px, 4vw, 48px);
|
padding: clamp(24px, 4vw, 48px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channels-shell, .settings-shell {
|
.channels-shell,
|
||||||
|
.settings-shell {
|
||||||
width: min(760px, 100%);
|
width: min(760px, 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channels-header, .settings-header {
|
.channels-header,
|
||||||
|
.settings-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -397,7 +400,8 @@ body {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-header h2, .download-header h3 {
|
.download-header h2,
|
||||||
|
.download-header h3 {
|
||||||
margin: 6px 0 8px;
|
margin: 6px 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +424,10 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-card-header {
|
.download-card-header {
|
||||||
@@ -466,7 +473,8 @@ body {
|
|||||||
margin: 16px 0 10px;
|
margin: 16px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button, button {
|
.button,
|
||||||
|
button {
|
||||||
background: #7c3aed;
|
background: #7c3aed;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
@@ -482,7 +490,9 @@ body {
|
|||||||
box-shadow: 0 10px 30px rgba(124, 58, 237, 0.25);
|
box-shadow: 0 10px 30px rgba(124, 58, 237, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:disabled, button:disabled, .button[aria-disabled="true"] {
|
.button:disabled,
|
||||||
|
button:disabled,
|
||||||
|
.button[aria-disabled="true"] {
|
||||||
background: #a78bfa;
|
background: #a78bfa;
|
||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -567,7 +577,9 @@ button:disabled:hover {
|
|||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-input:focus {
|
.text-input:focus {
|
||||||
@@ -576,7 +588,8 @@ button:disabled:hover {
|
|||||||
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.25);
|
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-input:disabled, .text-input[aria-disabled="true"] {
|
.text-input:disabled,
|
||||||
|
.text-input[aria-disabled="true"] {
|
||||||
background: #020617;
|
background: #020617;
|
||||||
border-color: #334155;
|
border-color: #334155;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
@@ -706,9 +719,9 @@ button:disabled:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-body {
|
.admin-body {
|
||||||
background: radial-gradient(circle at 0% 30%, rgba(59, 130, 246, 0.12), transparent 32%),
|
background:
|
||||||
radial-gradient(circle at 90% 5%, rgba(124, 58, 237, 0.1), transparent 30%),
|
radial-gradient(circle at 0% 30%, rgba(59, 130, 246, 0.12), transparent 32%),
|
||||||
#0f172a;
|
radial-gradient(circle at 90% 5%, rgba(124, 58, 237, 0.1), transparent 30%), #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-frame {
|
.admin-frame {
|
||||||
@@ -761,7 +774,9 @@ button:disabled:hover {
|
|||||||
.admin-rail {
|
.admin-rail {
|
||||||
background: rgba(11, 18, 32, 0.92);
|
background: rgba(11, 18, 32, 0.92);
|
||||||
border-right: 1px solid #1f2937;
|
border-right: 1px solid #1f2937;
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02), 0 18px 45px rgba(0, 0, 0, 0.4);
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.02),
|
||||||
|
0 18px 45px rgba(0, 0, 0, 0.4);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100vh - 81px);
|
height: calc(100vh - 81px);
|
||||||
@@ -822,9 +837,9 @@ button:disabled:hover {
|
|||||||
|
|
||||||
.dashboard-body {
|
.dashboard-body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: radial-gradient(circle at 0% 30%, rgba(59, 130, 246, 0.14), transparent 30%),
|
background:
|
||||||
radial-gradient(circle at 90% 10%, rgba(124, 58, 237, 0.12), transparent 28%),
|
radial-gradient(circle at 0% 30%, rgba(59, 130, 246, 0.14), transparent 30%),
|
||||||
#0f172a;
|
radial-gradient(circle at 90% 10%, rgba(124, 58, 237, 0.12), transparent 28%), #0f172a;
|
||||||
padding: 36px 18px 64px;
|
padding: 36px 18px 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1061,9 +1076,9 @@ button:disabled:hover {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: clamp(520px, 72vh, 980px);
|
min-height: clamp(520px, 72vh, 980px);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: radial-gradient(circle at 18% 20%, rgba(59, 130, 246, 0.08), transparent 38%),
|
background:
|
||||||
radial-gradient(circle at 80% 0%, rgba(124, 58, 237, 0.08), transparent 40%),
|
radial-gradient(circle at 18% 20%, rgba(59, 130, 246, 0.08), transparent 38%),
|
||||||
#0b1220;
|
radial-gradient(circle at 80% 0%, rgba(124, 58, 237, 0.08), transparent 40%), #0b1220;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #1f2937;
|
border: 1px solid #1f2937;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -1083,7 +1098,9 @@ button:disabled:hover {
|
|||||||
#admin-canvas {
|
#admin-canvas {
|
||||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.5), 0 16px 45px rgba(0, 0, 0, 0.55);
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(15, 23, 42, 0.5),
|
||||||
|
0 16px 45px rgba(0, 0, 0, 0.55);
|
||||||
background-color: rgba(255, 0, 255, 0.1);
|
background-color: rgba(255, 0, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1154,7 +1171,8 @@ button:disabled:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.canvas-boundary {
|
.canvas-boundary {
|
||||||
background-image: linear-gradient(to right, rgba(255,255,255,0.03) 1px, transparent 1px),
|
background-image:
|
||||||
|
linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
linear-gradient(to bottom, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||||
background-size: 24px 24px;
|
background-size: 24px 24px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1166,7 +1184,9 @@ button:disabled:hover {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), 0 16px 40px rgba(0, 0, 0, 0.35);
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.02),
|
||||||
|
0 16px 40px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-guides {
|
.canvas-guides {
|
||||||
@@ -1303,7 +1323,9 @@ button:disabled:hover {
|
|||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background: rgba(124, 58, 237, 0.08);
|
background: rgba(124, 58, 237, 0.08);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 120ms ease, background 120ms ease;
|
transition:
|
||||||
|
background 120ms ease,
|
||||||
|
background 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-input-trigger:hover {
|
.file-input-trigger:hover {
|
||||||
@@ -1826,7 +1848,10 @@ button:disabled:hover {
|
|||||||
background: linear-gradient(180deg, #e2e8f0, #cbd5e1);
|
background: linear-gradient(180deg, #e2e8f0, #cbd5e1);
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.18);
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.18);
|
||||||
transition: background 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
|
transition:
|
||||||
|
background 160ms ease,
|
||||||
|
border-color 160ms ease,
|
||||||
|
box-shadow 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-thumb {
|
.toggle-thumb {
|
||||||
@@ -1834,24 +1859,35 @@ button:disabled:hover {
|
|||||||
height: 17px;
|
height: 17px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18), 0 1px 0 rgba(255, 255, 255, 0.6) inset;
|
box-shadow:
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.18),
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.6) inset;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
transition:
|
||||||
|
transform 160ms ease,
|
||||||
|
box-shadow 160ms ease,
|
||||||
|
background 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-inline input[type="checkbox"]:checked + .toggle-track {
|
.checkbox-inline input[type="checkbox"]:checked + .toggle-track {
|
||||||
background: linear-gradient(180deg, #7c3aed, #342366);
|
background: linear-gradient(180deg, #7c3aed, #342366);
|
||||||
border-color: #7c3aed;
|
border-color: #7c3aed;
|
||||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(52, 199, 89, 0.35);
|
box-shadow:
|
||||||
|
inset 0 1px 2px rgba(0, 0, 0, 0.12),
|
||||||
|
0 0 0 1px rgba(52, 199, 89, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-inline input[type="checkbox"]:checked + .toggle-track .toggle-thumb {
|
.checkbox-inline input[type="checkbox"]:checked + .toggle-track .toggle-thumb {
|
||||||
transform: translateX(25px);
|
transform: translateX(25px);
|
||||||
box-shadow: 0 2px 6px rgba(40, 183, 75, 0.35), 0 1px 0 rgba(255, 255, 255, 0.6) inset;
|
box-shadow:
|
||||||
|
0 2px 6px rgba(40, 183, 75, 0.35),
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.6) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-inline input[type="checkbox"]:focus-visible + .toggle-track {
|
.checkbox-inline input[type="checkbox"]:focus-visible + .toggle-track {
|
||||||
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.35), inset 0 1px 2px rgba(0, 0, 0, 0.18);
|
box-shadow:
|
||||||
|
0 0 0 3px rgba(124, 58, 237, 0.35),
|
||||||
|
inset 0 1px 2px rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-label {
|
.toggle-label {
|
||||||
@@ -1960,7 +1996,9 @@ button:disabled:hover {
|
|||||||
background: #0b1221;
|
background: #0b1221;
|
||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 120ms ease, opacity 120ms ease;
|
transition:
|
||||||
|
transform 120ms ease,
|
||||||
|
opacity 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast:hover {
|
.toast:hover {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
|||||||
const canvas = document.getElementById('broadcast-canvas');
|
const canvas = document.getElementById("broadcast-canvas");
|
||||||
const obsBrowser = !!window.obsstudio;
|
const obsBrowser = !!window.obsstudio;
|
||||||
const supportsAnimatedDecode = typeof ImageDecoder !== 'undefined' && typeof createImageBitmap === 'function' && !obsBrowser;
|
const supportsAnimatedDecode =
|
||||||
const canPlayProbe = document.createElement('video');
|
typeof ImageDecoder !== "undefined" && typeof createImageBitmap === "function" && !obsBrowser;
|
||||||
const ctx = canvas.getContext('2d');
|
const canPlayProbe = document.createElement("video");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
let canvasSettings = { width: 1920, height: 1080 };
|
let canvasSettings = { width: 1920, height: 1080 };
|
||||||
canvas.width = canvasSettings.width;
|
canvas.width = canvasSettings.width;
|
||||||
canvas.height = canvasSettings.height;
|
canvas.height = canvasSettings.height;
|
||||||
@@ -23,7 +24,7 @@ let frameScheduled = false;
|
|||||||
let pendingDraw = false;
|
let pendingDraw = false;
|
||||||
let renderIntervalId = null;
|
let renderIntervalId = null;
|
||||||
const pendingRemovals = new Set();
|
const pendingRemovals = new Set();
|
||||||
const audioUnlockEvents = ['pointerdown', 'keydown', 'touchstart'];
|
const audioUnlockEvents = ["pointerdown", "keydown", "touchstart"];
|
||||||
let layerOrder = [];
|
let layerOrder = [];
|
||||||
|
|
||||||
audioUnlockEvents.forEach((eventName) => {
|
audioUnlockEvents.forEach((eventName) => {
|
||||||
@@ -34,19 +35,19 @@ audioUnlockEvents.forEach((eventName) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function ensureLayerPosition(assetId, placement = 'keep') {
|
function ensureLayerPosition(assetId, placement = "keep") {
|
||||||
const asset = assets.get(assetId);
|
const asset = assets.get(assetId);
|
||||||
if (asset && isAudioAsset(asset)) {
|
if (asset && isAudioAsset(asset)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const existingIndex = layerOrder.indexOf(assetId);
|
const existingIndex = layerOrder.indexOf(assetId);
|
||||||
if (existingIndex !== -1 && placement === 'keep') {
|
if (existingIndex !== -1 && placement === "keep") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
layerOrder.splice(existingIndex, 1);
|
layerOrder.splice(existingIndex, 1);
|
||||||
}
|
}
|
||||||
if (placement === 'append') {
|
if (placement === "append") {
|
||||||
layerOrder.push(assetId);
|
layerOrder.push(assetId);
|
||||||
} else {
|
} else {
|
||||||
layerOrder.unshift(assetId);
|
layerOrder.unshift(assetId);
|
||||||
@@ -71,7 +72,10 @@ function getLayerOrder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRenderOrder() {
|
function getRenderOrder() {
|
||||||
return [...getLayerOrder()].reverse().map((id) => assets.get(id)).filter(Boolean);
|
return [...getLayerOrder()]
|
||||||
|
.reverse()
|
||||||
|
.map((id) => assets.get(id))
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueRemoval(assetId) {
|
function queueRemoval(assetId) {
|
||||||
@@ -95,7 +99,7 @@ function flushPendingRemovals() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
const socket = new SockJS('/ws');
|
const socket = new SockJS("/ws");
|
||||||
const stompClient = Stomp.over(socket);
|
const stompClient = Stomp.over(socket);
|
||||||
stompClient.connect({}, () => {
|
stompClient.connect({}, () => {
|
||||||
stompClient.subscribe(`/topic/channel/${broadcaster}`, (payload) => {
|
stompClient.subscribe(`/topic/channel/${broadcaster}`, (payload) => {
|
||||||
@@ -105,22 +109,22 @@ function connect() {
|
|||||||
fetch(`/api/channels/${broadcaster}/assets/visible`)
|
fetch(`/api/channels/${broadcaster}/assets/visible`)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new Error('Failed to load assets');
|
throw new Error("Failed to load assets");
|
||||||
}
|
}
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
.then(renderAssets)
|
.then(renderAssets)
|
||||||
.catch(() => showToast('Unable to load overlay assets. Retrying may help.', 'error'));
|
.catch(() => showToast("Unable to load overlay assets. Retrying may help.", "error"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAssets(list) {
|
function renderAssets(list) {
|
||||||
layerOrder = [];
|
layerOrder = [];
|
||||||
list.forEach((asset) => storeAsset(asset, 'append'));
|
list.forEach((asset) => storeAsset(asset, "append"));
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeAsset(asset, placement = 'keep') {
|
function storeAsset(asset, placement = "keep") {
|
||||||
if (!asset) return;
|
if (!asset) return;
|
||||||
const wasExisting = assets.has(asset.id);
|
const wasExisting = assets.has(asset.id);
|
||||||
assets.set(asset.id, asset);
|
assets.set(asset.id, asset);
|
||||||
@@ -135,7 +139,7 @@ function fetchCanvasSettings() {
|
|||||||
return fetch(`/api/channels/${encodeURIComponent(broadcaster)}/canvas`)
|
return fetch(`/api/channels/${encodeURIComponent(broadcaster)}/canvas`)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new Error('Failed to load canvas');
|
throw new Error("Failed to load canvas");
|
||||||
}
|
}
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
@@ -145,7 +149,7 @@ function fetchCanvasSettings() {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
showToast('Using default canvas size. Unable to load saved settings.', 'warning');
|
showToast("Using default canvas size. Unable to load saved settings.", "warning");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,11 +168,11 @@ function resizeCanvas() {
|
|||||||
|
|
||||||
function handleEvent(event) {
|
function handleEvent(event) {
|
||||||
const assetId = event.assetId || event?.patch?.id || event?.payload?.id;
|
const assetId = event.assetId || event?.patch?.id || event?.payload?.id;
|
||||||
if (event.type === 'VISIBILITY') {
|
if (event.type === "VISIBILITY") {
|
||||||
handleVisibilityEvent(event);
|
handleVisibilityEvent(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.type === 'DELETED') {
|
if (event.type === "DELETED") {
|
||||||
removeAsset(assetId);
|
removeAsset(assetId);
|
||||||
} else if (event.patch) {
|
} else if (event.patch) {
|
||||||
applyPatch(assetId, event.patch);
|
applyPatch(assetId, event.patch);
|
||||||
@@ -177,10 +181,10 @@ function handleEvent(event) {
|
|||||||
if (payload.hidden) {
|
if (payload.hidden) {
|
||||||
hideAssetWithTransition(payload);
|
hideAssetWithTransition(payload);
|
||||||
} else if (!assets.has(payload.id)) {
|
} else if (!assets.has(payload.id)) {
|
||||||
upsertVisibleAsset(payload, 'append');
|
upsertVisibleAsset(payload, "append");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event.type === 'PLAY' && event.payload) {
|
} else if (event.type === "PLAY" && event.payload) {
|
||||||
const payload = normalizePayload(event.payload);
|
const payload = normalizePayload(event.payload);
|
||||||
storeAsset(payload);
|
storeAsset(payload);
|
||||||
if (isAudioAsset(payload)) {
|
if (isAudioAsset(payload)) {
|
||||||
@@ -205,7 +209,13 @@ function hideAssetWithTransition(asset) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const existing = assets.get(payload.id);
|
const existing = assets.get(payload.id);
|
||||||
if (!existing && (!Number.isFinite(payload.x) || !Number.isFinite(payload.y) || !Number.isFinite(payload.width) || !Number.isFinite(payload.height))) {
|
if (
|
||||||
|
!existing &&
|
||||||
|
(!Number.isFinite(payload.x) ||
|
||||||
|
!Number.isFinite(payload.y) ||
|
||||||
|
!Number.isFinite(payload.width) ||
|
||||||
|
!Number.isFinite(payload.height))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const merged = normalizePayload({ ...(existing || {}), ...payload, hidden: true });
|
const merged = normalizePayload({ ...(existing || {}), ...payload, hidden: true });
|
||||||
@@ -213,12 +223,12 @@ function hideAssetWithTransition(asset) {
|
|||||||
stopAudio(payload.id);
|
stopAudio(payload.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsertVisibleAsset(asset, placement = 'keep') {
|
function upsertVisibleAsset(asset, placement = "keep") {
|
||||||
const payload = asset ? normalizePayload(asset) : null;
|
const payload = asset ? normalizePayload(asset) : null;
|
||||||
if (!payload?.id) {
|
if (!payload?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const placementMode = assets.has(payload.id) ? 'keep' : placement;
|
const placementMode = assets.has(payload.id) ? "keep" : placement;
|
||||||
storeAsset(payload, placementMode);
|
storeAsset(payload, placementMode);
|
||||||
ensureMedia(payload);
|
ensureMedia(payload);
|
||||||
if (isAudioAsset(payload)) {
|
if (isAudioAsset(payload)) {
|
||||||
@@ -238,7 +248,7 @@ function handleVisibilityEvent(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
const placement = assets.has(payload.id) ? 'keep' : 'append';
|
const placement = assets.has(payload.id) ? "keep" : "append";
|
||||||
upsertVisibleAsset(payload, placement);
|
upsertVisibleAsset(payload, placement);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +264,7 @@ function applyPatch(assetId, patch) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sanitizedPatch = Object.fromEntries(
|
const sanitizedPatch = Object.fromEntries(
|
||||||
Object.entries(patch).filter(([, value]) => value !== null && value !== undefined)
|
Object.entries(patch).filter(([, value]) => value !== null && value !== undefined),
|
||||||
);
|
);
|
||||||
const existing = assets.get(assetId);
|
const existing = assets.get(assetId);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -266,9 +276,7 @@ function applyPatch(assetId, patch) {
|
|||||||
hideAssetWithTransition(merged);
|
hideAssetWithTransition(merged);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const targetLayer = Number.isFinite(patch.layer)
|
const targetLayer = Number.isFinite(patch.layer) ? patch.layer : Number.isFinite(patch.zIndex) ? patch.zIndex : null;
|
||||||
? patch.layer
|
|
||||||
: (Number.isFinite(patch.zIndex) ? patch.zIndex : null);
|
|
||||||
if (!isAudio && Number.isFinite(targetLayer)) {
|
if (!isAudio && Number.isFinite(targetLayer)) {
|
||||||
const currentOrder = getLayerOrder().filter((id) => id !== assetId);
|
const currentOrder = getLayerOrder().filter((id) => id !== assetId);
|
||||||
const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer));
|
const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer));
|
||||||
@@ -321,7 +329,7 @@ function drawAsset(asset) {
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalAlpha = Math.max(0, Math.min(1, visibility.alpha));
|
ctx.globalAlpha = Math.max(0, Math.min(1, visibility.alpha));
|
||||||
ctx.translate(renderState.x + halfWidth, renderState.y + halfHeight);
|
ctx.translate(renderState.x + halfWidth, renderState.y + halfHeight);
|
||||||
ctx.rotate(renderState.rotation * Math.PI / 180);
|
ctx.rotate((renderState.rotation * Math.PI) / 180);
|
||||||
|
|
||||||
if (isAudioAsset(asset)) {
|
if (isAudioAsset(asset)) {
|
||||||
if (!asset.hidden) {
|
if (!asset.hidden) {
|
||||||
@@ -360,7 +368,7 @@ function smoothState(asset) {
|
|||||||
y: lerp(previous.y, asset.y, factor),
|
y: lerp(previous.y, asset.y, factor),
|
||||||
width: lerp(previous.width, asset.width, factor),
|
width: lerp(previous.width, asset.width, factor),
|
||||||
height: lerp(previous.height, asset.height, factor),
|
height: lerp(previous.height, asset.height, factor),
|
||||||
rotation: smoothAngle(previous.rotation, asset.rotation, factor)
|
rotation: smoothAngle(previous.rotation, asset.rotation, factor),
|
||||||
};
|
};
|
||||||
renderStates.set(asset.id, next);
|
renderStates.set(asset.id, next);
|
||||||
return next;
|
return next;
|
||||||
@@ -404,19 +412,19 @@ function recordDuration(assetId, seconds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isVideoAsset(asset) {
|
function isVideoAsset(asset) {
|
||||||
return asset?.mediaType?.startsWith('video/');
|
return asset?.mediaType?.startsWith("video/");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAudioAsset(asset) {
|
function isAudioAsset(asset) {
|
||||||
return asset?.mediaType?.startsWith('audio/');
|
return asset?.mediaType?.startsWith("audio/");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVideoElement(element) {
|
function isVideoElement(element) {
|
||||||
return element && element.tagName === 'VIDEO';
|
return element && element.tagName === "VIDEO";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGifAsset(asset) {
|
function isGifAsset(asset) {
|
||||||
return asset?.mediaType?.toLowerCase() === 'image/gif';
|
return asset?.mediaType?.toLowerCase() === "image/gif";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDrawable(element) {
|
function isDrawable(element) {
|
||||||
@@ -429,7 +437,7 @@ function isDrawable(element) {
|
|||||||
if (isVideoElement(element)) {
|
if (isVideoElement(element)) {
|
||||||
return element.readyState >= 2;
|
return element.readyState >= 2;
|
||||||
}
|
}
|
||||||
if (typeof ImageBitmap !== 'undefined' && element instanceof ImageBitmap) {
|
if (typeof ImageBitmap !== "undefined" && element instanceof ImageBitmap) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return !!element.complete;
|
return !!element.complete;
|
||||||
@@ -438,7 +446,7 @@ function isDrawable(element) {
|
|||||||
function clearMedia(assetId) {
|
function clearMedia(assetId) {
|
||||||
const element = mediaCache.get(assetId);
|
const element = mediaCache.get(assetId);
|
||||||
if (isVideoElement(element)) {
|
if (isVideoElement(element)) {
|
||||||
element.src = '';
|
element.src = "";
|
||||||
element.remove();
|
element.remove();
|
||||||
}
|
}
|
||||||
mediaCache.delete(assetId);
|
mediaCache.delete(assetId);
|
||||||
@@ -463,7 +471,7 @@ function clearMedia(assetId) {
|
|||||||
}
|
}
|
||||||
audio.element.pause();
|
audio.element.pause();
|
||||||
audio.element.currentTime = 0;
|
audio.element.currentTime = 0;
|
||||||
audio.element.src = '';
|
audio.element.src = "";
|
||||||
audio.element.remove();
|
audio.element.remove();
|
||||||
audioControllers.delete(assetId);
|
audioControllers.delete(assetId);
|
||||||
}
|
}
|
||||||
@@ -482,9 +490,9 @@ function ensureAudioController(asset) {
|
|||||||
|
|
||||||
const element = new Audio(asset.url);
|
const element = new Audio(asset.url);
|
||||||
element.autoplay = true;
|
element.autoplay = true;
|
||||||
element.preload = 'auto';
|
element.preload = "auto";
|
||||||
element.controls = false;
|
element.controls = false;
|
||||||
element.addEventListener('loadedmetadata', () => recordDuration(asset.id, element.duration));
|
element.addEventListener("loadedmetadata", () => recordDuration(asset.id, element.duration));
|
||||||
const controller = {
|
const controller = {
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
src: asset.url,
|
src: asset.url,
|
||||||
@@ -493,7 +501,7 @@ function ensureAudioController(asset) {
|
|||||||
loopEnabled: false,
|
loopEnabled: false,
|
||||||
loopActive: true,
|
loopActive: true,
|
||||||
delayMs: 0,
|
delayMs: 0,
|
||||||
baseDelayMs: 0
|
baseDelayMs: 0,
|
||||||
};
|
};
|
||||||
element.onended = () => handleAudioEnded(asset.id);
|
element.onended = () => handleAudioEnded(asset.id);
|
||||||
audioControllers.set(asset.id, controller);
|
audioControllers.set(asset.id, controller);
|
||||||
@@ -577,7 +585,7 @@ function playAudioImmediately(asset) {
|
|||||||
function playOverlappingAudio(asset) {
|
function playOverlappingAudio(asset) {
|
||||||
const temp = new Audio(asset.url);
|
const temp = new Audio(asset.url);
|
||||||
temp.autoplay = true;
|
temp.autoplay = true;
|
||||||
temp.preload = 'auto';
|
temp.preload = "auto";
|
||||||
temp.controls = false;
|
temp.controls = false;
|
||||||
applyAudioElementSettings(temp, asset);
|
applyAudioElementSettings(temp, asset);
|
||||||
const controller = { element: temp };
|
const controller = { element: temp };
|
||||||
@@ -646,9 +654,9 @@ function ensureMedia(asset) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = isVideoAsset(asset) ? document.createElement('video') : new Image();
|
const element = isVideoAsset(asset) ? document.createElement("video") : new Image();
|
||||||
element.dataset.sourceUrl = asset.url;
|
element.dataset.sourceUrl = asset.url;
|
||||||
element.crossOrigin = 'anonymous';
|
element.crossOrigin = "anonymous";
|
||||||
if (isVideoElement(element)) {
|
if (isVideoElement(element)) {
|
||||||
if (!canPlayVideoType(asset.mediaType)) {
|
if (!canPlayVideoType(asset.mediaType)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -659,8 +667,8 @@ function ensureMedia(asset) {
|
|||||||
element.controls = false;
|
element.controls = false;
|
||||||
element.onloadeddata = draw;
|
element.onloadeddata = draw;
|
||||||
element.onloadedmetadata = () => recordDuration(asset.id, element.duration);
|
element.onloadedmetadata = () => recordDuration(asset.id, element.duration);
|
||||||
element.preload = 'auto';
|
element.preload = "auto";
|
||||||
element.addEventListener('error', () => clearMedia(asset.id));
|
element.addEventListener("error", () => clearMedia(asset.id));
|
||||||
applyMediaVolume(element, asset);
|
applyMediaVolume(element, asset);
|
||||||
element.muted = true;
|
element.muted = true;
|
||||||
setVideoSource(element, asset);
|
setVideoSource(element, asset);
|
||||||
@@ -696,11 +704,11 @@ function ensureAnimatedImage(asset) {
|
|||||||
bitmap: null,
|
bitmap: null,
|
||||||
timeout: null,
|
timeout: null,
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
isAnimated: true
|
isAnimated: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchAssetBlob(asset)
|
fetchAssetBlob(asset)
|
||||||
.then((blob) => new ImageDecoder({ data: blob, type: blob.type || 'image/gif' }))
|
.then((blob) => new ImageDecoder({ data: blob, type: blob.type || "image/gif" }))
|
||||||
.then((decoder) => {
|
.then((decoder) => {
|
||||||
if (controller.cancelled) {
|
if (controller.cancelled) {
|
||||||
decoder.close?.();
|
decoder.close?.();
|
||||||
@@ -753,13 +761,15 @@ function setVideoSource(element, asset) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAssetBlob(asset).then(() => {
|
fetchAssetBlob(asset)
|
||||||
|
.then(() => {
|
||||||
const next = blobCache.get(asset.id);
|
const next = blobCache.get(asset.id);
|
||||||
if (next?.url !== asset.url || !next.objectUrl) {
|
if (next?.url !== asset.url || !next.objectUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applyVideoSource(element, next.objectUrl, asset);
|
applyVideoSource(element, next.objectUrl, asset);
|
||||||
}).catch(() => { });
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyVideoSource(element, objectUrl, asset) {
|
function applyVideoSource(element, objectUrl, asset) {
|
||||||
@@ -776,7 +786,7 @@ function canPlayVideoType(mediaType) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const support = canPlayProbe.canPlayType(mediaType);
|
const support = canPlayProbe.canPlayType(mediaType);
|
||||||
return support === 'probably' || support === 'maybe';
|
return support === "probably" || support === "maybe";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCachedSource(element) {
|
function getCachedSource(element) {
|
||||||
@@ -787,7 +797,9 @@ function scheduleNextFrame(controller) {
|
|||||||
if (controller.cancelled || !controller.decoder) {
|
if (controller.cancelled || !controller.decoder) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
controller.decoder.decode().then(({ image, complete }) => {
|
controller.decoder
|
||||||
|
.decode()
|
||||||
|
.then(({ image, complete }) => {
|
||||||
if (controller.cancelled) {
|
if (controller.cancelled) {
|
||||||
image.close?.();
|
image.close?.();
|
||||||
return;
|
return;
|
||||||
@@ -814,7 +826,8 @@ function scheduleNextFrame(controller) {
|
|||||||
scheduleNextFrame(controller);
|
scheduleNextFrame(controller);
|
||||||
}
|
}
|
||||||
}, delay);
|
}, delay);
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
// If decoding fails, clear animated cache so static fallback is used next render
|
// If decoding fails, clear animated cache so static fallback is used next render
|
||||||
animatedCache.delete(controller.id);
|
animatedCache.delete(controller.id);
|
||||||
animationFailures.set(controller.id, Date.now());
|
animationFailures.set(controller.id, Date.now());
|
||||||
@@ -849,9 +862,13 @@ function startVideoPlayback(element, asset) {
|
|||||||
if (!element.paused && element.readyState >= 2) {
|
if (!element.paused && element.readyState >= 2) {
|
||||||
element.muted = false;
|
element.muted = false;
|
||||||
} else {
|
} else {
|
||||||
element.addEventListener('playing', () => {
|
element.addEventListener(
|
||||||
|
"playing",
|
||||||
|
() => {
|
||||||
element.muted = false;
|
element.muted = false;
|
||||||
}, { once: true });
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -865,7 +882,7 @@ function startRenderLoop() {
|
|||||||
}, MIN_FRAME_TIME);
|
}, MIN_FRAME_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener("resize", () => {
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
function buildIdentity(admin) {
|
function buildIdentity(admin) {
|
||||||
const identity = document.createElement('div');
|
const identity = document.createElement("div");
|
||||||
identity.className = 'identity-row';
|
identity.className = "identity-row";
|
||||||
|
|
||||||
const avatar = document.createElement(admin.avatarUrl ? 'img' : 'div');
|
const avatar = document.createElement(admin.avatarUrl ? "img" : "div");
|
||||||
avatar.className = 'avatar';
|
avatar.className = "avatar";
|
||||||
if (admin.avatarUrl) {
|
if (admin.avatarUrl) {
|
||||||
avatar.src = admin.avatarUrl;
|
avatar.src = admin.avatarUrl;
|
||||||
avatar.alt = `${admin.displayName || admin.login} avatar`;
|
avatar.alt = `${admin.displayName || admin.login} avatar`;
|
||||||
} else {
|
} else {
|
||||||
avatar.classList.add('avatar-fallback');
|
avatar.classList.add("avatar-fallback");
|
||||||
avatar.textContent = (admin.displayName || admin.login || '?').charAt(0).toUpperCase();
|
avatar.textContent = (admin.displayName || admin.login || "?").charAt(0).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
const details = document.createElement('div');
|
const details = document.createElement("div");
|
||||||
details.className = 'identity-text';
|
details.className = "identity-text";
|
||||||
const title = document.createElement('p');
|
const title = document.createElement("p");
|
||||||
title.className = 'list-title';
|
title.className = "list-title";
|
||||||
title.textContent = admin.displayName || admin.login;
|
title.textContent = admin.displayName || admin.login;
|
||||||
const subtitle = document.createElement('p');
|
const subtitle = document.createElement("p");
|
||||||
subtitle.className = 'muted';
|
subtitle.className = "muted";
|
||||||
subtitle.textContent = `@${admin.login}`;
|
subtitle.textContent = `@${admin.login}`;
|
||||||
|
|
||||||
details.appendChild(title);
|
details.appendChild(title);
|
||||||
@@ -29,30 +29,30 @@ function buildIdentity(admin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAdmins(list) {
|
function renderAdmins(list) {
|
||||||
const adminList = document.getElementById('admin-list');
|
const adminList = document.getElementById("admin-list");
|
||||||
if (!adminList) return;
|
if (!adminList) return;
|
||||||
adminList.innerHTML = '';
|
adminList.innerHTML = "";
|
||||||
if (!list || list.length === 0) {
|
if (!list || list.length === 0) {
|
||||||
const empty = document.createElement('li');
|
const empty = document.createElement("li");
|
||||||
empty.textContent = 'No channel admins yet';
|
empty.textContent = "No channel admins yet";
|
||||||
adminList.appendChild(empty);
|
adminList.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.forEach((admin) => {
|
list.forEach((admin) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement("li");
|
||||||
li.className = 'stacked-list-item';
|
li.className = "stacked-list-item";
|
||||||
|
|
||||||
li.appendChild(buildIdentity(admin));
|
li.appendChild(buildIdentity(admin));
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement("div");
|
||||||
actions.className = 'actions';
|
actions.className = "actions";
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement("button");
|
||||||
removeBtn.type = 'button';
|
removeBtn.type = "button";
|
||||||
removeBtn.className = 'secondary';
|
removeBtn.className = "secondary";
|
||||||
removeBtn.textContent = 'Remove';
|
removeBtn.textContent = "Remove";
|
||||||
removeBtn.addEventListener('click', () => removeAdmin(admin.login));
|
removeBtn.addEventListener("click", () => removeAdmin(admin.login));
|
||||||
|
|
||||||
actions.appendChild(removeBtn);
|
actions.appendChild(removeBtn);
|
||||||
li.appendChild(actions);
|
li.appendChild(actions);
|
||||||
@@ -61,32 +61,32 @@ function renderAdmins(list) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSuggestedAdmins(list) {
|
function renderSuggestedAdmins(list) {
|
||||||
const suggestionList = document.getElementById('admin-suggestions');
|
const suggestionList = document.getElementById("admin-suggestions");
|
||||||
if (!suggestionList) return;
|
if (!suggestionList) return;
|
||||||
|
|
||||||
suggestionList.innerHTML = '';
|
suggestionList.innerHTML = "";
|
||||||
if (!list || list.length === 0) {
|
if (!list || list.length === 0) {
|
||||||
const empty = document.createElement('li');
|
const empty = document.createElement("li");
|
||||||
empty.className = 'stacked-list-item';
|
empty.className = "stacked-list-item";
|
||||||
empty.textContent = 'No moderator suggestions right now';
|
empty.textContent = "No moderator suggestions right now";
|
||||||
suggestionList.appendChild(empty);
|
suggestionList.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.forEach((admin) => {
|
list.forEach((admin) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement("li");
|
||||||
li.className = 'stacked-list-item';
|
li.className = "stacked-list-item";
|
||||||
|
|
||||||
li.appendChild(buildIdentity(admin));
|
li.appendChild(buildIdentity(admin));
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement("div");
|
||||||
actions.className = 'actions';
|
actions.className = "actions";
|
||||||
|
|
||||||
const addBtn = document.createElement('button');
|
const addBtn = document.createElement("button");
|
||||||
addBtn.type = 'button';
|
addBtn.type = "button";
|
||||||
addBtn.className = 'ghost';
|
addBtn.className = "ghost";
|
||||||
addBtn.textContent = 'Add as admin';
|
addBtn.textContent = "Add as admin";
|
||||||
addBtn.addEventListener('click', () => addAdmin(admin.login));
|
addBtn.addEventListener("click", () => addAdmin(admin.login));
|
||||||
|
|
||||||
actions.appendChild(addBtn);
|
actions.appendChild(addBtn);
|
||||||
li.appendChild(actions);
|
li.appendChild(actions);
|
||||||
@@ -98,7 +98,7 @@ function fetchSuggestedAdmins() {
|
|||||||
fetch(`/api/channels/${broadcaster}/admins/suggestions`)
|
fetch(`/api/channels/${broadcaster}/admins/suggestions`)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new Error('Failed to load admin suggestions');
|
throw new Error("Failed to load admin suggestions");
|
||||||
}
|
}
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
@@ -112,62 +112,64 @@ function fetchAdmins() {
|
|||||||
fetch(`/api/channels/${broadcaster}/admins`)
|
fetch(`/api/channels/${broadcaster}/admins`)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new Error('Failed to load admins');
|
throw new Error("Failed to load admins");
|
||||||
}
|
}
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
.then(renderAdmins)
|
.then(renderAdmins)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
renderAdmins([]);
|
renderAdmins([]);
|
||||||
showToast('Unable to load admins right now. Please try again.', 'error');
|
showToast("Unable to load admins right now. Please try again.", "error");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAdmin(username) {
|
function removeAdmin(username) {
|
||||||
if (!username) return;
|
if (!username) return;
|
||||||
fetch(`/api/channels/${encodeURIComponent(broadcaster)}/admins/${encodeURIComponent(username)}`, {
|
fetch(`/api/channels/${encodeURIComponent(broadcaster)}/admins/${encodeURIComponent(username)}`, {
|
||||||
method: 'DELETE'
|
method: "DELETE",
|
||||||
}).then((response) => {
|
})
|
||||||
|
.then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
fetchAdmins();
|
fetchAdmins();
|
||||||
fetchSuggestedAdmins();
|
fetchSuggestedAdmins();
|
||||||
}).catch(() => {
|
})
|
||||||
showToast('Failed to remove admin. Please retry.', 'error');
|
.catch(() => {
|
||||||
|
showToast("Failed to remove admin. Please retry.", "error");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addAdmin(usernameFromAction) {
|
function addAdmin(usernameFromAction) {
|
||||||
const input = document.getElementById('new-admin');
|
const input = document.getElementById("new-admin");
|
||||||
const username = (usernameFromAction || input?.value || '').trim();
|
const username = (usernameFromAction || input?.value || "").trim();
|
||||||
if (!username) {
|
if (!username) {
|
||||||
showToast('Enter a Twitch username to add as an admin.', 'info');
|
showToast("Enter a Twitch username to add as an admin.", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(`/api/channels/${broadcaster}/admins`, {
|
fetch(`/api/channels/${broadcaster}/admins`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username })
|
body: JSON.stringify({ username }),
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Add admin failed');
|
throw new Error("Add admin failed");
|
||||||
}
|
}
|
||||||
if (input) {
|
if (input) {
|
||||||
input.value = '';
|
input.value = "";
|
||||||
}
|
}
|
||||||
showToast(`Added @${username} as an admin.`, 'success');
|
showToast(`Added @${username} as an admin.`, "success");
|
||||||
fetchAdmins();
|
fetchAdmins();
|
||||||
fetchSuggestedAdmins();
|
fetchSuggestedAdmins();
|
||||||
})
|
})
|
||||||
.catch(() => showToast('Unable to add admin right now. Please try again.', 'error'));
|
.catch(() => showToast("Unable to add admin right now. Please try again.", "error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCanvasSettings(settings) {
|
function renderCanvasSettings(settings) {
|
||||||
const widthInput = document.getElementById('canvas-width');
|
const widthInput = document.getElementById("canvas-width");
|
||||||
const heightInput = document.getElementById('canvas-height');
|
const heightInput = document.getElementById("canvas-height");
|
||||||
if (widthInput) widthInput.value = Math.round(settings.width);
|
if (widthInput) widthInput.value = Math.round(settings.width);
|
||||||
if (heightInput) heightInput.value = Math.round(settings.height);
|
if (heightInput) heightInput.value = Math.round(settings.height);
|
||||||
}
|
}
|
||||||
@@ -176,50 +178,50 @@ function fetchCanvasSettings() {
|
|||||||
fetch(`/api/channels/${broadcaster}/canvas`)
|
fetch(`/api/channels/${broadcaster}/canvas`)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new Error('Failed to load canvas settings');
|
throw new Error("Failed to load canvas settings");
|
||||||
}
|
}
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
.then(renderCanvasSettings)
|
.then(renderCanvasSettings)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
renderCanvasSettings({ width: 1920, height: 1080 });
|
renderCanvasSettings({ width: 1920, height: 1080 });
|
||||||
showToast('Using default canvas size. Unable to load saved settings.', 'warning');
|
showToast("Using default canvas size. Unable to load saved settings.", "warning");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCanvasSettings() {
|
function saveCanvasSettings() {
|
||||||
const widthInput = document.getElementById('canvas-width');
|
const widthInput = document.getElementById("canvas-width");
|
||||||
const heightInput = document.getElementById('canvas-height');
|
const heightInput = document.getElementById("canvas-height");
|
||||||
const status = document.getElementById('canvas-status');
|
const status = document.getElementById("canvas-status");
|
||||||
const width = parseFloat(widthInput?.value) || 0;
|
const width = parseFloat(widthInput?.value) || 0;
|
||||||
const height = parseFloat(heightInput?.value) || 0;
|
const height = parseFloat(heightInput?.value) || 0;
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
showToast('Please enter a valid width and height.', 'info');
|
showToast("Please enter a valid width and height.", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status) status.textContent = 'Saving...';
|
if (status) status.textContent = "Saving...";
|
||||||
fetch(`/api/channels/${broadcaster}/canvas`, {
|
fetch(`/api/channels/${broadcaster}/canvas`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ width, height })
|
body: JSON.stringify({ width, height }),
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new Error('Failed to save canvas');
|
throw new Error("Failed to save canvas");
|
||||||
}
|
}
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
.then((settings) => {
|
.then((settings) => {
|
||||||
renderCanvasSettings(settings);
|
renderCanvasSettings(settings);
|
||||||
if (status) status.textContent = 'Saved.';
|
if (status) status.textContent = "Saved.";
|
||||||
showToast('Canvas size saved successfully.', 'success');
|
showToast("Canvas size saved successfully.", "success");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (status) status.textContent = '';
|
if (status) status.textContent = "";
|
||||||
}, 2000);
|
}, 2000);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (status) status.textContent = 'Unable to save right now.';
|
if (status) status.textContent = "Unable to save right now.";
|
||||||
showToast('Unable to save canvas size. Please retry.', 'error');
|
showToast("Unable to save canvas size. Please retry.", "error");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
function detectPlatform() {
|
function detectPlatform() {
|
||||||
const navigatorPlatform = (navigator.userAgentData?.platform || navigator.platform || '').toLowerCase();
|
const navigatorPlatform = (navigator.userAgentData?.platform || navigator.platform || "").toLowerCase();
|
||||||
const userAgent = (navigator.userAgent || '').toLowerCase();
|
const userAgent = (navigator.userAgent || "").toLowerCase();
|
||||||
const platformString = `${navigatorPlatform} ${userAgent}`;
|
const platformString = `${navigatorPlatform} ${userAgent}`;
|
||||||
|
|
||||||
if (platformString.includes('mac') || platformString.includes('darwin')) {
|
if (platformString.includes("mac") || platformString.includes("darwin")) {
|
||||||
return 'mac';
|
return "mac";
|
||||||
}
|
}
|
||||||
if (platformString.includes('win')) {
|
if (platformString.includes("win")) {
|
||||||
return 'windows';
|
return "windows";
|
||||||
}
|
}
|
||||||
if (platformString.includes('linux')) {
|
if (platformString.includes("linux")) {
|
||||||
return 'linux';
|
return "linux";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function markRecommendedDownload(section) {
|
function markRecommendedDownload(section) {
|
||||||
const cards = Array.from(section.querySelectorAll('.download-card'));
|
const cards = Array.from(section.querySelectorAll(".download-card"));
|
||||||
if (!cards.length) {
|
if (!cards.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -26,15 +26,15 @@ function markRecommendedDownload(section) {
|
|||||||
|
|
||||||
cards.forEach((card) => {
|
cards.forEach((card) => {
|
||||||
const isPreferred = card === preferredCard;
|
const isPreferred = card === preferredCard;
|
||||||
card.classList.toggle('download-card--active', isPreferred);
|
card.classList.toggle("download-card--active", isPreferred);
|
||||||
const badge = card.querySelector('.recommended-badge');
|
const badge = card.querySelector(".recommended-badge");
|
||||||
if (badge) {
|
if (badge) {
|
||||||
badge.classList.toggle('hidden', !isPreferred);
|
badge.classList.toggle("hidden", !isPreferred);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const downloadSections = document.querySelectorAll('.download-section, .download-card-block');
|
const downloadSections = document.querySelectorAll(".download-section, .download-card-block");
|
||||||
downloadSections.forEach(markRecommendedDownload);
|
downloadSections.forEach(markRecommendedDownload);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
function updateSuggestions(term) {
|
function updateSuggestions(term) {
|
||||||
const normalizedTerm = term.trim().toLowerCase();
|
const normalizedTerm = term.trim().toLowerCase();
|
||||||
const filtered = channels
|
const filtered = channels.filter((name) => !normalizedTerm || name.includes(normalizedTerm)).slice(0, 20);
|
||||||
.filter((name) => !normalizedTerm || name.includes(normalizedTerm))
|
|
||||||
.slice(0, 20);
|
|
||||||
|
|
||||||
suggestions.innerHTML = "";
|
suggestions.innerHTML = "";
|
||||||
filtered.forEach((name) => {
|
filtered.forEach((name) => {
|
||||||
|
|||||||
@@ -105,12 +105,16 @@ function submitSettingsForm() {
|
|||||||
}
|
}
|
||||||
statusElement.textContent = "Saving…";
|
statusElement.textContent = "Saving…";
|
||||||
statusElement.classList.remove("status-success", "status-warning");
|
statusElement.classList.remove("status-success", "status-warning");
|
||||||
fetch("/api/settings/set", { method: "PUT", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userSettings) }).then((r) => {
|
fetch("/api/settings/set", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(userSettings),
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new Error('Failed to load canvas');
|
throw new Error("Failed to load canvas");
|
||||||
}
|
}
|
||||||
return r.json();
|
return r.json();
|
||||||
|
|
||||||
})
|
})
|
||||||
.then((newSettings) => {
|
.then((newSettings) => {
|
||||||
currentSettings = { ...newSettings };
|
currentSettings = { ...newSettings };
|
||||||
@@ -122,7 +126,7 @@ function submitSettingsForm() {
|
|||||||
updateSubmitButtonDisabledState();
|
updateSubmitButtonDisabledState();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
showToast('Unable to save settings', 'error')
|
showToast("Unable to save settings", "error");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
statusElement.textContent = "Save failed. Try again.";
|
statusElement.textContent = "Save failed. Try again.";
|
||||||
statusElement.classList.add("status-warning");
|
statusElement.classList.add("status-warning");
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const CONTAINER_ID = 'toast-container';
|
const CONTAINER_ID = "toast-container";
|
||||||
const DEFAULT_DURATION = 4200;
|
const DEFAULT_DURATION = 4200;
|
||||||
|
|
||||||
function ensureContainer() {
|
function ensureContainer() {
|
||||||
let container = document.getElementById(CONTAINER_ID);
|
let container = document.getElementById(CONTAINER_ID);
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = document.createElement('div');
|
container = document.createElement("div");
|
||||||
container.id = CONTAINER_ID;
|
container.id = CONTAINER_ID;
|
||||||
container.className = 'toast-container';
|
container.className = "toast-container";
|
||||||
container.setAttribute('aria-live', 'polite');
|
container.setAttribute("aria-live", "polite");
|
||||||
container.setAttribute('aria-atomic', 'true');
|
container.setAttribute("aria-atomic", "true");
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
}
|
}
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildToast(message, type) {
|
function buildToast(message, type) {
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement("div");
|
||||||
toast.className = `toast toast-${type}`;
|
toast.className = `toast toast-${type}`;
|
||||||
|
|
||||||
const indicator = document.createElement('span');
|
const indicator = document.createElement("span");
|
||||||
indicator.className = 'toast-indicator';
|
indicator.className = "toast-indicator";
|
||||||
indicator.setAttribute('aria-hidden', 'true');
|
indicator.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
const content = document.createElement('div');
|
const content = document.createElement("div");
|
||||||
content.className = 'toast-message';
|
content.className = "toast-message";
|
||||||
content.textContent = message;
|
content.textContent = message;
|
||||||
|
|
||||||
toast.appendChild(indicator);
|
toast.appendChild(indicator);
|
||||||
@@ -34,18 +34,18 @@
|
|||||||
|
|
||||||
function removeToast(toast) {
|
function removeToast(toast) {
|
||||||
if (!toast) return;
|
if (!toast) return;
|
||||||
toast.classList.add('toast-exit');
|
toast.classList.add("toast-exit");
|
||||||
setTimeout(() => toast.remove(), 250);
|
setTimeout(() => toast.remove(), 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.showToast = function showToast(message, type = 'info', options = {}) {
|
window.showToast = function showToast(message, type = "info", options = {}) {
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
const normalized = ['success', 'error', 'warning', 'info'].includes(type) ? type : 'info';
|
const normalized = ["success", "error", "warning", "info"].includes(type) ? type : "info";
|
||||||
const duration = typeof options.duration === 'number' ? options.duration : DEFAULT_DURATION;
|
const duration = typeof options.duration === "number" ? options.duration : DEFAULT_DURATION;
|
||||||
const container = ensureContainer();
|
const container = ensureContainer();
|
||||||
const toast = buildToast(message, normalized);
|
const toast = buildToast(message, normalized);
|
||||||
container.appendChild(toast);
|
container.appendChild(toast);
|
||||||
setTimeout(() => removeToast(toast), Math.max(1200, duration));
|
setTimeout(() => removeToast(toast), Math.max(1200, duration));
|
||||||
toast.addEventListener('click', () => removeToast(toast));
|
toast.addEventListener("click", () => removeToast(toast));
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Imgfloat Admin</title>
|
<title>Imgfloat Admin</title>
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
||||||
|
integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A=="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -22,14 +28,22 @@
|
|||||||
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
|
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
|
||||||
<span class="sr-only">Back to dashboard</span>
|
<span class="sr-only">Back to dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="button ghost" th:href="${'/view/' + broadcaster + '/broadcast'}" target="_blank" rel="noopener">Broadcaster view</a>
|
<a class="button ghost" th:href="${'/view/' + broadcaster + '/broadcast'}" target="_blank" rel="noopener"
|
||||||
|
>Broadcaster view</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="admin-workspace">
|
<div class="admin-workspace">
|
||||||
<aside class="admin-rail">
|
<aside class="admin-rail">
|
||||||
<div class="upload-row">
|
<div class="upload-row">
|
||||||
<input id="asset-file" class="file-input-field" type="file" accept="image/*,video/*,audio/*" onchange="handleFileSelection(this)" />
|
<input
|
||||||
|
id="asset-file"
|
||||||
|
class="file-input-field"
|
||||||
|
type="file"
|
||||||
|
accept="image/*,video/*,audio/*"
|
||||||
|
onchange="handleFileSelection(this)"
|
||||||
|
/>
|
||||||
<label for="asset-file" class="file-input-trigger">
|
<label for="asset-file" class="file-input-trigger">
|
||||||
<span class="file-input-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
|
<span class="file-input-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
|
||||||
<span class="file-input-copy">
|
<span class="file-input-copy">
|
||||||
@@ -52,7 +66,9 @@
|
|||||||
<strong id="selected-asset-name">Choose an asset</strong>
|
<strong id="selected-asset-name">Choose an asset</strong>
|
||||||
<span id="selected-asset-resolution" class="asset-resolution subtle-text hidden"></span>
|
<span id="selected-asset-resolution" class="asset-resolution subtle-text hidden"></span>
|
||||||
</div>
|
</div>
|
||||||
<p class="meta-text" id="selected-asset-meta">Pick an asset in the list to adjust its placement and playback.</p>
|
<p class="meta-text" id="selected-asset-meta">
|
||||||
|
Pick an asset in the list to adjust its placement and playback.
|
||||||
|
</p>
|
||||||
<p class="meta-text subtle-text hidden" id="selected-asset-id"></p>
|
<p class="meta-text subtle-text hidden" id="selected-asset-id"></p>
|
||||||
<div class="badge-row asset-meta-badges" id="selected-asset-badges"></div>
|
<div class="badge-row asset-meta-badges" id="selected-asset-badges"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +86,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<span class="property-label">Height</span>
|
<span class="property-label">Height</span>
|
||||||
<input id="asset-height" class="number-input property-control" type="number" min="10" step="5" />
|
<input
|
||||||
|
id="asset-height"
|
||||||
|
class="number-input property-control"
|
||||||
|
type="number"
|
||||||
|
min="10"
|
||||||
|
step="5"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<span class="property-label">Maintain AR</span>
|
<span class="property-label">Maintain AR</span>
|
||||||
@@ -101,7 +123,15 @@
|
|||||||
<span>Playback speed</span>
|
<span>Playback speed</span>
|
||||||
<span class="value-hint" id="asset-speed-label">100%</span>
|
<span class="value-hint" id="asset-speed-label">100%</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="asset-speed" class="range-input" type="range" min="0" max="1000" step="10" value="100" />
|
<input
|
||||||
|
id="asset-speed"
|
||||||
|
class="range-input"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1000"
|
||||||
|
step="10"
|
||||||
|
value="100"
|
||||||
|
/>
|
||||||
<div class="range-meta"><span>0%</span><span>1000%</span></div>
|
<div class="range-meta"><span>0%</span><span>1000%</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,7 +145,15 @@
|
|||||||
<span>Playback volume</span>
|
<span>Playback volume</span>
|
||||||
<span class="value-hint" id="asset-volume-label">100%</span>
|
<span class="value-hint" id="asset-volume-label">100%</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="asset-volume" class="range-input" type="range" min="0" max="200" step="1" value="100" />
|
<input
|
||||||
|
id="asset-volume"
|
||||||
|
class="range-input"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="200"
|
||||||
|
step="1"
|
||||||
|
value="100"
|
||||||
|
/>
|
||||||
<div class="range-meta"><span>0%</span><span>200%</span></div>
|
<div class="range-meta"><span>0%</span><span>200%</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +178,15 @@
|
|||||||
<span>Delay</span>
|
<span>Delay</span>
|
||||||
<span class="value-hint" id="asset-audio-delay-label">0ms</span>
|
<span class="value-hint" id="asset-audio-delay-label">0ms</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="asset-audio-delay" class="range-input property-control" type="range" min="0" max="30000" step="100" value="0" />
|
<input
|
||||||
|
id="asset-audio-delay"
|
||||||
|
class="range-input property-control"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="30000"
|
||||||
|
step="100"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
<div class="range-meta"><span>0ms</span><span>30s</span></div>
|
<div class="range-meta"><span>0ms</span><span>30s</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stacked-field">
|
<div class="stacked-field">
|
||||||
@@ -148,7 +194,15 @@
|
|||||||
<span>Playback speed</span>
|
<span>Playback speed</span>
|
||||||
<span class="value-hint" id="asset-audio-speed-label">1.0x</span>
|
<span class="value-hint" id="asset-audio-speed-label">1.0x</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="asset-audio-speed" class="range-input" type="range" min="25" max="400" step="5" value="100" />
|
<input
|
||||||
|
id="asset-audio-speed"
|
||||||
|
class="range-input"
|
||||||
|
type="range"
|
||||||
|
min="25"
|
||||||
|
max="400"
|
||||||
|
step="5"
|
||||||
|
value="100"
|
||||||
|
/>
|
||||||
<div class="range-meta"><span>0.25x</span><span>4x</span></div>
|
<div class="range-meta"><span>0.25x</span><span>4x</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stacked-field">
|
<div class="stacked-field">
|
||||||
@@ -156,22 +210,58 @@
|
|||||||
<span>Pitch</span>
|
<span>Pitch</span>
|
||||||
<span class="value-hint" id="asset-audio-pitch-label">100%</span>
|
<span class="value-hint" id="asset-audio-pitch-label">100%</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="asset-audio-pitch" class="range-input property-control" type="range" min="50" max="200" step="5" value="100" />
|
<input
|
||||||
|
id="asset-audio-pitch"
|
||||||
|
class="range-input property-control"
|
||||||
|
type="range"
|
||||||
|
min="50"
|
||||||
|
max="200"
|
||||||
|
step="5"
|
||||||
|
value="100"
|
||||||
|
/>
|
||||||
<div class="range-meta"><span>50%</span><span>200%</span></div>
|
<div class="range-meta"><span>50%</span><span>200%</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-actions compact unified-actions" id="asset-actions">
|
<div class="control-actions compact unified-actions" id="asset-actions">
|
||||||
<button type="button" onclick="sendToBack()" class="secondary" title="Send to back"><i class="fa-solid fa-angles-down"></i></button>
|
<button type="button" onclick="sendToBack()" class="secondary" title="Send to back">
|
||||||
<button type="button" onclick="bringBackward()" class="secondary" title="Move backward"><i class="fa-solid fa-arrow-down"></i></button>
|
<i class="fa-solid fa-angles-down"></i>
|
||||||
<button type="button" onclick="bringForward()" class="secondary" title="Move forward"><i class="fa-solid fa-arrow-up"></i></button>
|
</button>
|
||||||
<button type="button" onclick="bringToFront()" class="secondary" title="Bring to front"><i class="fa-solid fa-angles-up"></i></button>
|
<button type="button" onclick="bringBackward()" class="secondary" title="Move backward">
|
||||||
<button type="button" onclick="recenterSelectedAsset()" class="secondary" title="Center on canvas"><i class="fa-solid fa-bullseye"></i></button>
|
<i class="fa-solid fa-arrow-down"></i>
|
||||||
<button type="button" onclick="nudgeRotation(-5)" class="secondary" title="Rotate left"><i class="fa-solid fa-rotate-left"></i></button>
|
</button>
|
||||||
<button type="button" onclick="nudgeRotation(5)" class="secondary" title="Rotate right"><i class="fa-solid fa-rotate-right"></i></button>
|
<button type="button" onclick="bringForward()" class="secondary" title="Move forward">
|
||||||
<button id="selected-asset-visibility" class="secondary" type="button" title="Hide asset" disabled data-audio-enabled="true">
|
<i class="fa-solid fa-arrow-up"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="bringToFront()" class="secondary" title="Bring to front">
|
||||||
|
<i class="fa-solid fa-angles-up"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="recenterSelectedAsset()" class="secondary" title="Center on canvas">
|
||||||
|
<i class="fa-solid fa-bullseye"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="nudgeRotation(-5)" class="secondary" title="Rotate left">
|
||||||
|
<i class="fa-solid fa-rotate-left"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="nudgeRotation(5)" class="secondary" title="Rotate right">
|
||||||
|
<i class="fa-solid fa-rotate-right"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="selected-asset-visibility"
|
||||||
|
class="secondary"
|
||||||
|
type="button"
|
||||||
|
title="Hide asset"
|
||||||
|
disabled
|
||||||
|
data-audio-enabled="true"
|
||||||
|
>
|
||||||
<i class="fa-solid fa-eye-slash"></i>
|
<i class="fa-solid fa-eye-slash"></i>
|
||||||
</button>
|
</button>
|
||||||
<button id="selected-asset-delete" class="secondary danger" type="button" title="Delete asset" disabled data-audio-enabled="true">
|
<button
|
||||||
|
id="selected-asset-delete"
|
||||||
|
class="secondary danger"
|
||||||
|
type="button"
|
||||||
|
title="Delete asset"
|
||||||
|
disabled
|
||||||
|
data-audio-enabled="true"
|
||||||
|
>
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Imgfloat Broadcast</title>
|
<title>Imgfloat Broadcast</title>
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<body class="broadcast-body">
|
<body class="broadcast-body">
|
||||||
<canvas id="broadcast-canvas"></canvas>
|
<canvas id="broadcast-canvas"></canvas>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
const broadcaster = /*[[${broadcaster}]]*/ '';
|
const broadcaster = /*[[${broadcaster}]]*/ "";
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/toast.js"></script>
|
<script src="/js/toast.js"></script>
|
||||||
<script src="/js/broadcast.js"></script>
|
<script src="/js/broadcast.js"></script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Browse channels - Imgfloat</title>
|
<title>Browse channels - Imgfloat</title>
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
@@ -24,7 +24,15 @@
|
|||||||
<p class="muted">Type the channel name to jump straight to their overlay.</p>
|
<p class="muted">Type the channel name to jump straight to their overlay.</p>
|
||||||
<form id="channel-search-form" class="channel-form">
|
<form id="channel-search-form" class="channel-form">
|
||||||
<label class="sr-only" for="channel-search">Channel name</label>
|
<label class="sr-only" for="channel-search">Channel name</label>
|
||||||
<input id="channel-search" name="channel" class="text-input" type="text" list="channel-suggestions" placeholder="Type a channel name" autocomplete="off" />
|
<input
|
||||||
|
id="channel-search"
|
||||||
|
name="channel"
|
||||||
|
class="text-input"
|
||||||
|
type="text"
|
||||||
|
list="channel-suggestions"
|
||||||
|
placeholder="Type a channel name"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
<datalist id="channel-suggestions"></datalist>
|
<datalist id="channel-suggestions"></datalist>
|
||||||
<button type="submit" class="button block">Open overlay</button>
|
<button type="submit" class="button block">Open overlay</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Imgfloat Dashboard</title>
|
<title>Imgfloat Dashboard</title>
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
@@ -110,7 +110,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Desktop app</p>
|
<p class="eyebrow">Desktop app</p>
|
||||||
<h3>Download Imgfloat</h3>
|
<h3>Download Imgfloat</h3>
|
||||||
<p class="muted">Version <span class="version-inline" th:text="${releaseVersion}">0.0.1</span> · build <span class="version-inline" th:text="${version}">unknown</span></p>
|
<p class="muted">
|
||||||
|
Version <span class="version-inline" th:text="${releaseVersion}">0.0.1</span> · build
|
||||||
|
<span class="version-inline" th:text="${version}">unknown</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="download-grid">
|
<div class="download-grid">
|
||||||
@@ -120,7 +123,11 @@
|
|||||||
<span class="badge soft recommended-badge hidden">Recommended</span>
|
<span class="badge soft recommended-badge hidden">Recommended</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Apple Silicon build (ARM64)</p>
|
<p class="muted">Apple Silicon build (ARM64)</p>
|
||||||
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '-arm64.dmg'">Download .dmg</a>
|
<a
|
||||||
|
class="button block"
|
||||||
|
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '-arm64.dmg'"
|
||||||
|
>Download .dmg</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="download-card" data-platform="windows">
|
<div class="download-card" data-platform="windows">
|
||||||
<div class="download-card-header">
|
<div class="download-card-header">
|
||||||
@@ -128,7 +135,11 @@
|
|||||||
<span class="badge soft recommended-badge hidden">Recommended</span>
|
<span class="badge soft recommended-badge hidden">Recommended</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Installer for Windows 10 and 11</p>
|
<p class="muted">Installer for Windows 10 and 11</p>
|
||||||
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat.Setup.' + ${releaseVersion} + '.exe'">Download .exe</a>
|
<a
|
||||||
|
class="button block"
|
||||||
|
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat.Setup.' + ${releaseVersion} + '.exe'"
|
||||||
|
>Download .exe</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="download-card" data-platform="linux">
|
<div class="download-card" data-platform="linux">
|
||||||
<div class="download-card-header">
|
<div class="download-card-header">
|
||||||
@@ -136,7 +147,11 @@
|
|||||||
<span class="badge soft recommended-badge hidden">Recommended</span>
|
<span class="badge soft recommended-badge hidden">Recommended</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">AppImage for most distributions</p>
|
<p class="muted">AppImage for most distributions</p>
|
||||||
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '.AppImage'">Download AppImage</a>
|
<a
|
||||||
|
class="button block"
|
||||||
|
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '.AppImage'"
|
||||||
|
>Download AppImage</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -144,7 +159,7 @@
|
|||||||
<script src="/js/toast.js"></script>
|
<script src="/js/toast.js"></script>
|
||||||
<script src="/js/downloads.js"></script>
|
<script src="/js/downloads.js"></script>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
const broadcaster = /*[[${channel}]]*/ '';
|
const broadcaster = /*[[${channel}]]*/ "";
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/dashboard.js"></script>
|
<script src="/js/dashboard.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Imgfloat - Twitch overlay</title>
|
<title>Imgfloat - Twitch overlay</title>
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
@@ -21,7 +21,9 @@
|
|||||||
<div class="hero-text">
|
<div class="hero-text">
|
||||||
<p class="eyebrow">Overlay toolkit</p>
|
<p class="eyebrow">Overlay toolkit</p>
|
||||||
<h1>Keep your Twitch overlays tidy.</h1>
|
<h1>Keep your Twitch overlays tidy.</h1>
|
||||||
<p class="lead">Upload artwork, drop it into a shared dashboard, and stay in sync with your mods without clutter.</p>
|
<p class="lead">
|
||||||
|
Upload artwork, drop it into a shared dashboard, and stay in sync with your mods without clutter.
|
||||||
|
</p>
|
||||||
<div class="cta-row">
|
<div class="cta-row">
|
||||||
<a class="button" href="/oauth2/authorization/twitch">Login with Twitch</a>
|
<a class="button" href="/oauth2/authorization/twitch">Login with Twitch</a>
|
||||||
<a class="button ghost" href="/channels" rel="prefetch">Browse channels</a>
|
<a class="button ghost" href="/channels" rel="prefetch">Browse channels</a>
|
||||||
@@ -33,7 +35,9 @@
|
|||||||
<div class="download-header">
|
<div class="download-header">
|
||||||
<p class="eyebrow">Desktop app</p>
|
<p class="eyebrow">Desktop app</p>
|
||||||
<h2>Download Imgfloat</h2>
|
<h2>Download Imgfloat</h2>
|
||||||
<p class="muted">Version <span class="version-inline" th:text="${releaseVersion}">0.0.1</span> for Windows, macOS, and Linux.</p>
|
<p class="muted">
|
||||||
|
Version <span class="version-inline" th:text="${releaseVersion}">0.0.1</span> for Windows, macOS, and Linux.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="download-grid">
|
<div class="download-grid">
|
||||||
<div class="download-card" data-platform="mac">
|
<div class="download-card" data-platform="mac">
|
||||||
@@ -42,7 +46,11 @@
|
|||||||
<span class="badge soft recommended-badge hidden">Recommended</span>
|
<span class="badge soft recommended-badge hidden">Recommended</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Apple Silicon build (ARM64)</p>
|
<p class="muted">Apple Silicon build (ARM64)</p>
|
||||||
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '-arm64.dmg'">Download .dmg</a>
|
<a
|
||||||
|
class="button block"
|
||||||
|
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '-arm64.dmg'"
|
||||||
|
>Download .dmg</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="download-card" data-platform="windows">
|
<div class="download-card" data-platform="windows">
|
||||||
<div class="download-card-header">
|
<div class="download-card-header">
|
||||||
@@ -50,7 +58,11 @@
|
|||||||
<span class="badge soft recommended-badge hidden">Recommended</span>
|
<span class="badge soft recommended-badge hidden">Recommended</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Installer for Windows 10 and 11</p>
|
<p class="muted">Installer for Windows 10 and 11</p>
|
||||||
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat.Setup.' + ${releaseVersion} + '.exe'">Download .exe</a>
|
<a
|
||||||
|
class="button block"
|
||||||
|
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat.Setup.' + ${releaseVersion} + '.exe'"
|
||||||
|
>Download .exe</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="download-card" data-platform="linux">
|
<div class="download-card" data-platform="linux">
|
||||||
<div class="download-card-header">
|
<div class="download-card-header">
|
||||||
@@ -58,7 +70,11 @@
|
|||||||
<span class="badge soft recommended-badge hidden">Recommended</span>
|
<span class="badge soft recommended-badge hidden">Recommended</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">AppImage for most distributions</p>
|
<p class="muted">AppImage for most distributions</p>
|
||||||
<a class="button block" th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '.AppImage'">Download AppImage</a>
|
<a
|
||||||
|
class="button block"
|
||||||
|
th:href="'https://github.com/Kruhlmann/imgfloat-j/releases/download/' + ${releaseVersion} + '/Imgfloat-' + ${releaseVersion} + '.AppImage'"
|
||||||
|
>Download AppImage</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Imgfloat Admin</title>
|
<title>Imgfloat Admin</title>
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
||||||
|
integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A=="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -26,8 +32,8 @@
|
|||||||
<p class="eyebrow subtle">System administrator settings</p>
|
<p class="eyebrow subtle">System administrator settings</p>
|
||||||
<h1>Application defaults</h1>
|
<h1>Application defaults</h1>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
Configure overlay performance and audio guardrails for every channel using Imgfloat.
|
Configure overlay performance and audio guardrails for every channel using Imgfloat. These settings are
|
||||||
These settings are applied globally.
|
applied globally.
|
||||||
</p>
|
</p>
|
||||||
<div class="badge-row">
|
<div class="badge-row">
|
||||||
<span class="badge soft"><i class="fa-solid fa-gauge-high"></i> Performance tuned</span>
|
<span class="badge soft"><i class="fa-solid fa-gauge-high"></i> Performance tuned</span>
|
||||||
@@ -74,10 +80,13 @@
|
|||||||
<div class="form-heading">
|
<div class="form-heading">
|
||||||
<p class="eyebrow subtle">Canvas</p>
|
<p class="eyebrow subtle">Canvas</p>
|
||||||
<h3>Rendering budget</h3>
|
<h3>Rendering budget</h3>
|
||||||
<p class="muted tiny">Match FPS and max dimensions to your streaming canvas for consistent overlays.</p>
|
<p class="muted tiny">
|
||||||
|
Match FPS and max dimensions to your streaming canvas for consistent overlays.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-grid split-row">
|
<div class="control-grid split-row">
|
||||||
<label for="canvas-fps">Canvas FPS
|
<label for="canvas-fps"
|
||||||
|
>Canvas FPS
|
||||||
<input
|
<input
|
||||||
id="canvas-fps"
|
id="canvas-fps"
|
||||||
name="canvas-fps"
|
name="canvas-fps"
|
||||||
@@ -89,7 +98,8 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label for="canvas-size">Canvas max side length (pixels)
|
<label for="canvas-size"
|
||||||
|
>Canvas max side length (pixels)
|
||||||
<input
|
<input
|
||||||
id="canvas-size"
|
id="canvas-size"
|
||||||
name="canvas-size"
|
name="canvas-size"
|
||||||
@@ -111,7 +121,8 @@
|
|||||||
<p class="muted tiny">Bound default speeds between 0 and 1 so clips run predictably.</p>
|
<p class="muted tiny">Bound default speeds between 0 and 1 so clips run predictably.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-grid split-row">
|
<div class="control-grid split-row">
|
||||||
<label for="min-playback-speed">Min playback speed
|
<label for="min-playback-speed"
|
||||||
|
>Min playback speed
|
||||||
<input
|
<input
|
||||||
id="min-playback-speed"
|
id="min-playback-speed"
|
||||||
name="min-playback-speed"
|
name="min-playback-speed"
|
||||||
@@ -123,7 +134,8 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label for="max-playback-speed">Max playback speed
|
<label for="max-playback-speed"
|
||||||
|
>Max playback speed
|
||||||
<input
|
<input
|
||||||
id="max-playback-speed"
|
id="max-playback-speed"
|
||||||
name="max-playback-speed"
|
name="max-playback-speed"
|
||||||
@@ -135,7 +147,9 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="field-hint">Keep the maximum at 1.0 to avoid speeding overlays beyond their source frame rate.</p>
|
<p class="field-hint">
|
||||||
|
Keep the maximum at 1.0 to avoid speeding overlays beyond their source frame rate.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
@@ -145,7 +159,8 @@
|
|||||||
<p class="muted tiny">Prevent harsh audio by bounding pitch and volume as fractions of the source.</p>
|
<p class="muted tiny">Prevent harsh audio by bounding pitch and volume as fractions of the source.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-grid split-row">
|
<div class="control-grid split-row">
|
||||||
<label for="min-audio-pitch">Min audio pitch
|
<label for="min-audio-pitch"
|
||||||
|
>Min audio pitch
|
||||||
<input
|
<input
|
||||||
id="min-audio-pitch"
|
id="min-audio-pitch"
|
||||||
name="min-audio-pitch"
|
name="min-audio-pitch"
|
||||||
@@ -157,7 +172,8 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label for="max-audio-pitch">Max audio pitch
|
<label for="max-audio-pitch"
|
||||||
|
>Max audio pitch
|
||||||
<input
|
<input
|
||||||
id="max-audio-pitch"
|
id="max-audio-pitch"
|
||||||
name="max-audio-pitch"
|
name="max-audio-pitch"
|
||||||
@@ -170,7 +186,8 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-grid split-row">
|
<div class="control-grid split-row">
|
||||||
<label for="min-volume">Min volume
|
<label for="min-volume"
|
||||||
|
>Min volume
|
||||||
<input
|
<input
|
||||||
id="min-volume"
|
id="min-volume"
|
||||||
name="min-volume"
|
name="min-volume"
|
||||||
@@ -182,7 +199,8 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label for="max-volume">Max volume
|
<label for="max-volume"
|
||||||
|
>Max volume
|
||||||
<input
|
<input
|
||||||
id="max-volume"
|
id="max-volume"
|
||||||
name="max-volume"
|
name="max-volume"
|
||||||
@@ -219,7 +237,9 @@
|
|||||||
<section class="settings-card info-card subtle">
|
<section class="settings-card info-card subtle">
|
||||||
<p class="eyebrow subtle">Heads up</p>
|
<p class="eyebrow subtle">Heads up</p>
|
||||||
<h3>Global impact</h3>
|
<h3>Global impact</h3>
|
||||||
<p class="muted tiny">Changes here update every channel immediately. Save carefully and confirm with your team.</p>
|
<p class="muted tiny">
|
||||||
|
Changes here update every channel immediately. Save carefully and confirm with your team.
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user