Add prettier

This commit is contained in:
2026-01-05 11:34:53 +01:00
parent c0ca04a349
commit 65488ae59e
18 changed files with 4981 additions and 4662 deletions

13
.prettierrc Normal file
View 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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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