diff --git a/CHANGELOG.md b/CHANGELOG.md index d098c028d..cd605a3d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ # Changelog All notable changes to Mainsail will be documented in this file. +## [2.11.2](https://github.com/mainsail-crew/mainsail/releases/tag/v2.11.2) - 2024-05-04 +### Bug Fixes and Improvements + +- **maintenance**: Fix overdue check from printtime based entries ([#1871](https://github.com/mainsail-crew/mainsail/pull/1871)) +- **spoolman**: Fix search for spool-id ([#1872](https://github.com/mainsail-crew/mainsail/pull/1872)) +- Calc multiplicator for set_pin gcode ([#1870](https://github.com/mainsail-crew/mainsail/pull/1870)) + ## [2.11.1](https://github.com/mainsail-crew/mainsail/releases/tag/v2.11.1) - 2024-05-01 ### Bug Fixes and Improvements diff --git a/package-lock.json b/package-lock.json index e50a8a14d..0785ec110 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mainsail", - "version": "2.11.2", + "version": "2.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mainsail", - "version": "2.11.2", + "version": "2.12.0", "dependencies": { "@codemirror/commands": "^6.0.1", "@codemirror/lang-css": "^6.0.0", @@ -18,13 +18,13 @@ "@codemirror/view": "^6.0.3", "@jaames/iro": "^5.5.2", "@lezer/highlight": "^1.0.0", - "@sindarius/gcodeviewer": "^3.7.8", + "@sindarius/gcodeviewer": "^3.7.11", "@uiw/codemirror-theme-vscode": "^4.19.11", "axios": "^1.6.0", "codemirror": "^6.0.1", "core-js": "^3.16.0", "detect-browser": "^5.3.0", - "echarts": "^5.2.2", + "echarts": "^5.5.0", "echarts-gl": "^2.0.8", "hls.js": "^1.3.3", "jmuxer": "^2.0.5", @@ -2137,76 +2137,76 @@ } }, "node_modules/@babylonjs/core": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-6.43.0.tgz", - "integrity": "sha512-pLFvvtC26TeICUcF0YjuuQqvCahJwPlATXnJkvBzMWXXxUE0Y5rI8L28iScu2WKy975605KdccJI5VL2NG7sUQ==" + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-7.13.1.tgz", + "integrity": "sha512-limlsRIhRBH9xsuUNsy9xAyi0jhfQxfvhlMzMjFK3Ugq4c7joYpoZMkQU038esOQ3aq3q8VPv1+CshE3NASEMQ==" }, "node_modules/@babylonjs/gui": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@babylonjs/gui/-/gui-6.43.0.tgz", - "integrity": "sha512-iYsB7uyC40w0QkFGcPw8jAzE26jsgTFvWHTVKPSZxjQ5cG2RQEeK3aD8l+GvWauSc2qPB0wMOsvkAtusdy1g7g==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@babylonjs/gui/-/gui-7.13.1.tgz", + "integrity": "sha512-FW2QOqpzoJI2q/hFKOa7O7xnj6oKzHWmTGHVhqjg7jzZy//bMZ5AbxNdMpQgDa+QoENpd7r+yx1O4kc4Lw1UaQ==", "peer": true, "peerDependencies": { - "@babylonjs/core": "^6.0.0" + "@babylonjs/core": "^7.0.0" } }, "node_modules/@babylonjs/gui-editor": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@babylonjs/gui-editor/-/gui-editor-6.43.0.tgz", - "integrity": "sha512-OtahbC04Mklj2i2WdGlYXc9oN/ntpp2DuNZhqc1l95ioPIZWtlhSzMQKzh6/VmJQZBwwRi8HZwPPjQ1arIkRHA==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@babylonjs/gui-editor/-/gui-editor-7.13.1.tgz", + "integrity": "sha512-1piYccMR4FRqMXVu/OMqCgc6Z5uRpt49u6yUYyRY627aZkarBM7oh310A6sgTkfuk77c9ZNoIgtgbq5P/dB48Q==", "peer": true, "peerDependencies": { - "@babylonjs/core": "^6.0.0", - "@babylonjs/gui": "^6.0.0", + "@babylonjs/core": "^7.0.0", + "@babylonjs/gui": "^7.0.0", "@types/react": ">=16.7.3", "@types/react-dom": ">=16.0.9" } }, "node_modules/@babylonjs/inspector": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@babylonjs/inspector/-/inspector-6.43.0.tgz", - "integrity": "sha512-Cjw/Xk8v5/84IFi6rhLS363XgDkQzPrfQG7nFKHF6k8OWwz8Ze/cb8O8IeXKEc2QFImCdRnZoDv7BkJnZ03Caw==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@babylonjs/inspector/-/inspector-7.13.1.tgz", + "integrity": "sha512-BM2ZQu15ESbWFQPwU/iqCx1P/KiJX9qX9dCVU/+5bbLqzemja5aDajLMnM+vb4uimAtbokkUdFqNckGnZVo7VQ==", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.1.0", "@fortawesome/free-regular-svg-icons": "^6.0.0", "@fortawesome/free-solid-svg-icons": "^6.0.0" }, "peerDependencies": { - "@babylonjs/core": "^6.0.0", - "@babylonjs/gui": "^6.0.0", - "@babylonjs/gui-editor": "^6.0.0", - "@babylonjs/loaders": "^6.0.0", - "@babylonjs/materials": "^6.0.0", - "@babylonjs/serializers": "^6.0.0", + "@babylonjs/core": "^7.0.0", + "@babylonjs/gui": "^7.0.0", + "@babylonjs/gui-editor": "^7.0.0", + "@babylonjs/loaders": "^7.0.0", + "@babylonjs/materials": "^7.0.0", + "@babylonjs/serializers": "^7.0.0", "@types/react": ">=16.7.3", "@types/react-dom": ">=16.0.9" } }, "node_modules/@babylonjs/loaders": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-6.43.0.tgz", - "integrity": "sha512-+cgw4NDchjc/5dZYGzOEhJU+9++ygYlA+ChybE5yvF+yUDWZ7fGXnEXMFImXeFELjk5RRK1NS3H3YvPe7A+Rug==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-7.13.1.tgz", + "integrity": "sha512-17MCwYpMM4EF69wzsclIvs7Ci84lYdkcH2NSM3hD3phl8k373yNKVvBNrs68p9yZUCFbXIVEH1tl5QQxr2T37g==", "peerDependencies": { - "@babylonjs/core": "^6.0.0", - "babylonjs-gltf2interface": "^6.0.0" + "@babylonjs/core": "^7.0.0", + "babylonjs-gltf2interface": "^7.0.0" } }, "node_modules/@babylonjs/materials": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@babylonjs/materials/-/materials-6.43.0.tgz", - "integrity": "sha512-x4pKz2d0IRFZ9pmWIJ4FR/X5mak8Ycl2tAQtOf1ihsanNyDgXnB3Jfgl9z9TaoQSoU2y+HpjvenDF4oaLkXzUw==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@babylonjs/materials/-/materials-7.13.1.tgz", + "integrity": "sha512-CkjHyhsMglr9Aqf5NNsqwCgVPdMxnu1yRD4I6CGDXAp814ghqb5/JwkoL4lgelSYinsSh4HXpFr4Jgays5W2hw==", "peerDependencies": { - "@babylonjs/core": "^6.0.0" + "@babylonjs/core": "^7.0.0" } }, "node_modules/@babylonjs/serializers": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@babylonjs/serializers/-/serializers-6.43.0.tgz", - "integrity": "sha512-cVFGO98SapRf/QJRQx7+hZkbFnDhXuHK9obDJKKpv/76vSpLWDoLoZk1v1Nu8pm30n75Ukffcco77Za14ZZhFA==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@babylonjs/serializers/-/serializers-7.13.1.tgz", + "integrity": "sha512-ct1MTjRZaj4EEyMhDiHJkhSSKJRGValGPLNv3UOYh7EYc4XN0HbEz4wuMkwlgENFP7Hz5okWVBQB9yQWObR3Sg==", "peer": true, "peerDependencies": { - "@babylonjs/core": "^6.0.0", - "babylonjs-gltf2interface": "^6.0.0" + "@babylonjs/core": "^7.0.0", + "babylonjs-gltf2interface": "^7.0.0" } }, "node_modules/@codemirror/autocomplete": { @@ -2826,45 +2826,45 @@ } }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", - "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", "hasInstallScript": true, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", - "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", + "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.5.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", - "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz", + "integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.5.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", - "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", + "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.5.2" }, "engines": { "node": ">=6" @@ -3198,14 +3198,14 @@ "dev": true }, "node_modules/@sindarius/gcodeviewer": { - "version": "3.7.8", - "resolved": "https://registry.npmjs.org/@sindarius/gcodeviewer/-/gcodeviewer-3.7.8.tgz", - "integrity": "sha512-/Z9YE/GaVBV4dydju9kJ1uv8/GlffUzjoIGYxlj+kvZnBhP7OZ0ZWviL7zs4d7npSwSw7J+LNYWaIpVW13KQrA==", - "dependencies": { - "@babylonjs/core": "^6.43.0", - "@babylonjs/inspector": "^6.43.0", - "@babylonjs/loaders": "^6.43.0", - "@babylonjs/materials": "^6.43.0", + "version": "3.7.11", + "resolved": "https://registry.npmjs.org/@sindarius/gcodeviewer/-/gcodeviewer-3.7.11.tgz", + "integrity": "sha512-8Fd0yYBguxgljhJueysIDI0Hkh7bCsBTwfWAAyMqSwwTalcf7UpfaF0lmBE5y6f4tkBjjLy+oE6oGUkLHsP0Bg==", + "dependencies": { + "@babylonjs/core": "^7.13.1", + "@babylonjs/inspector": "^7.13.1", + "@babylonjs/loaders": "^7.13.1", + "@babylonjs/materials": "^7.13.1", "d3": "^7.4.4" } }, @@ -3294,26 +3294,25 @@ "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", "peer": true }, "node_modules/@types/react": { - "version": "18.2.56", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.56.tgz", - "integrity": "sha512-NpwHDMkS/EFZF2dONFQHgkPRwhvgq/OAvIaGQzxGSBmaeR++kTg6njr15Vatz0/2VcCEwJQFi6Jf4Q0qBu0rLA==", + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", "peer": true, "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.2.19", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", - "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "peer": true, "dependencies": { "@types/react": "*" @@ -3328,12 +3327,6 @@ "@types/node": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "peer": true - }, "node_modules/@types/semver": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", @@ -4226,9 +4219,9 @@ } }, "node_modules/babylonjs-gltf2interface": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-6.43.0.tgz", - "integrity": "sha512-+orCb4giE41Ysl8d2ZOeYCRBsT6FnmDwkW9+sHcEACUEpdtFeTShZ+ckucDAB/Ia2e3ah9F2hADbGA/8TfFngg==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-7.13.1.tgz", + "integrity": "sha512-kgMZrek1Gul22+Igy43pYdofg89odPv5uxYrjzryVvMxmzPI7NwgxickXT3tM/SGoyF0AoXlPrKLCK5zHT0/eg==", "peer": true }, "node_modules/balanced-match": { @@ -4302,12 +4295,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -5396,12 +5389,12 @@ } }, "node_modules/echarts": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.3.tgz", - "integrity": "sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz", + "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", "dependencies": { "tslib": "2.3.0", - "zrender": "5.4.4" + "zrender": "5.5.0" } }, "node_modules/echarts-gl": { @@ -6071,9 +6064,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -10820,9 +10813,9 @@ } }, "node_modules/zrender": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.4.tgz", - "integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz", + "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", "dependencies": { "tslib": "2.3.0" } diff --git a/package.json b/package.json index 4c7508dc9..aa74e82d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mainsail", - "version": "2.11.6", + "version": "2.12.1", "private": true, "decription": "a klipper web interface", "author": { @@ -33,13 +33,13 @@ "@codemirror/view": "^6.0.3", "@jaames/iro": "^5.5.2", "@lezer/highlight": "^1.0.0", - "@sindarius/gcodeviewer": "^3.7.8", + "@sindarius/gcodeviewer": "^3.7.11", "@uiw/codemirror-theme-vscode": "^4.19.11", "axios": "^1.6.0", "codemirror": "^6.0.1", "core-js": "^3.16.0", "detect-browser": "^5.3.0", - "echarts": "^5.2.2", + "echarts": "^5.5.0", "echarts-gl": "^2.0.8", "hls.js": "^1.3.3", "jmuxer": "^2.0.5", diff --git a/public/config.json b/public/config.json index 9dd1d5a8d..d28bd65eb 100644 --- a/public/config.json +++ b/public/config.json @@ -1,6 +1,7 @@ { "defaultLocale": "en", - "defaultTheme": "dark", + "defaultMode": "dark", + "defaultTheme": "mainsail", "hostname": null, "port": null, "path": null, diff --git a/public/img/themes/sidebarBackground-vzbot.png b/public/img/themes/sidebarBackground-vzbot.png new file mode 100644 index 000000000..872de6f77 Binary files /dev/null and b/public/img/themes/sidebarBackground-vzbot.png differ diff --git a/public/img/themes/sidebarLogo-btt.svg b/public/img/themes/sidebarLogo-btt.svg new file mode 100644 index 000000000..667134f19 --- /dev/null +++ b/public/img/themes/sidebarLogo-btt.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/public/img/themes/sidebarLogo-klipper.svg b/public/img/themes/sidebarLogo-klipper.svg new file mode 100644 index 000000000..64cdbb9d1 --- /dev/null +++ b/public/img/themes/sidebarLogo-klipper.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/img/themes/sidebarLogo-ldo.svg b/public/img/themes/sidebarLogo-ldo.svg new file mode 100644 index 000000000..d9a80686d --- /dev/null +++ b/public/img/themes/sidebarLogo-ldo.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/img/themes/sidebarLogo-multec.svg b/public/img/themes/sidebarLogo-multec.svg new file mode 100644 index 000000000..7269011ad --- /dev/null +++ b/public/img/themes/sidebarLogo-multec.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/themes/sidebarLogo-prusa.svg b/public/img/themes/sidebarLogo-prusa.svg new file mode 100644 index 000000000..41284153e --- /dev/null +++ b/public/img/themes/sidebarLogo-prusa.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/img/themes/sidebarLogo-voron.svg b/public/img/themes/sidebarLogo-voron.svg new file mode 100644 index 000000000..3dff67ca7 --- /dev/null +++ b/public/img/themes/sidebarLogo-voron.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/img/themes/sidebarLogo-vzbot.svg b/public/img/themes/sidebarLogo-vzbot.svg new file mode 100644 index 000000000..fdb7d4368 --- /dev/null +++ b/public/img/themes/sidebarLogo-vzbot.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/img/themes/sidebarLogo-yumi.svg b/public/img/themes/sidebarLogo-yumi.svg new file mode 100644 index 000000000..0b3c6b4ce --- /dev/null +++ b/public/img/themes/sidebarLogo-yumi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 90bf3c45d..0ca5f7c55 100644 --- a/src/App.vue +++ b/src/App.vue @@ -81,10 +81,6 @@ export default class App extends Mixins(BaseMixin, ThemeMixin) { return this.$store.getters['getTitle'] } - get mainBackground(): string { - return this.$store.getters['files/getMainBackground'] - } - get naviDrawer(): boolean { return this.$store.state.naviDrawer } @@ -98,8 +94,8 @@ export default class App extends Mixins(BaseMixin, ThemeMixin) { paddingLeft: '0', } - if (this.mainBackground !== null) { - style.backgroundImage = 'url(' + this.mainBackground + ')' + if (this.mainBgImage !== null) { + style.backgroundImage = 'url(' + this.mainBgImage + ')' } // overwrite padding left for the sidebar @@ -127,8 +123,8 @@ export default class App extends Mixins(BaseMixin, ThemeMixin) { return this.$store.state.printer.print_stats?.filename ?? '' } - get theme(): string { - return this.$store.state.gui.uiSettings.theme + get mode(): string { + return this.$store.state.gui.uiSettings.mode } get logoColor(): string { @@ -231,8 +227,8 @@ export default class App extends Mixins(BaseMixin, ThemeMixin) { }) } - @Watch('theme') - themeChanged(newVal: string): void { + @Watch('mode') + modeChanged(newVal: string): void { const dark = newVal !== 'light' this.$vuetify.theme.dark = dark @@ -240,73 +236,105 @@ export default class App extends Mixins(BaseMixin, ThemeMixin) { doc.className = dark ? 'theme--dark' : 'theme--light' } - drawFavicon(val: number): void { + async drawFavicon(val: number): Promise { const favicon16: HTMLLinkElement | null = document.querySelector("link[rel*='icon'][sizes='16x16']") const favicon32: HTMLLinkElement | null = document.querySelector("link[rel*='icon'][sizes='32x32']") - if (favicon16 && favicon32) { - if (this.progressAsFavicon && this.printerIsPrinting) { - let faviconSize = 64 - - let canvas = document.createElement('canvas') - canvas.width = faviconSize - canvas.height = faviconSize - const context = canvas.getContext('2d') - const centerX = canvas.width / 2 - const centerY = canvas.height / 2 - const radius = 32 - - // draw the grey circle - if (context) { - context.beginPath() - context.moveTo(centerX, centerY) - context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false) - context.closePath() - context.fillStyle = '#ddd' - context.fill() - context.strokeStyle = 'rgba(200, 208, 218, 0.66)' - context.stroke() - - // draw the green circle based on percentage - let startAngle = 1.5 * Math.PI - let endAngle = 0 - let unitValue = (Math.PI - 0.5 * Math.PI) / 25 - if (val >= 0 && val <= 25) endAngle = startAngle + val * unitValue - else if (val > 25 && val <= 50) endAngle = startAngle + val * unitValue - else if (val > 50 && val <= 75) endAngle = startAngle + val * unitValue - else if (val > 75 && val <= 100) endAngle = startAngle + val * unitValue - - context.beginPath() - context.moveTo(centerX, centerY) - context.arc(centerX, centerY, radius, startAngle, endAngle, false) - context.closePath() - context.fillStyle = this.logoColor - context.fill() - - favicon16.href = canvas.toDataURL('image/png') - favicon32.href = canvas.toDataURL('image/png') - } - } else if (this.customFavicons) { - const [favicon16Path, favicon32Path] = this.customFavicons - favicon16.href = favicon16Path - favicon32.href = favicon32Path - } else { - const favicon = - 'data:image/svg+xml;base64,' + - window.btoa(` - - - - - - - - `) - - favicon16.href = favicon - favicon32.href = favicon + // if no favicon is found, stop + if (!favicon16 || !favicon32) return + + // if progressAsFavicon is enabled and the printer is printing, draw the progress as favicon + if (this.progressAsFavicon && this.printerIsPrinting) { + let faviconSize = 64 + + let canvas = document.createElement('canvas') + canvas.width = faviconSize + canvas.height = faviconSize + const context = canvas.getContext('2d') + const centerX = canvas.width / 2 + const centerY = canvas.height / 2 + const radius = 32 + + if (!context) return + + // draw the grey circle + context.beginPath() + context.moveTo(centerX, centerY) + context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false) + context.closePath() + context.fillStyle = '#ddd' + context.fill() + context.strokeStyle = 'rgba(200, 208, 218, 0.66)' + context.stroke() + + // draw the green circle based on percentage + let startAngle = 1.5 * Math.PI + let endAngle = 0 + let unitValue = (Math.PI - 0.5 * Math.PI) / 25 + if (val >= 0 && val <= 25) endAngle = startAngle + val * unitValue + else if (val > 25 && val <= 50) endAngle = startAngle + val * unitValue + else if (val > 50 && val <= 75) endAngle = startAngle + val * unitValue + else if (val > 75 && val <= 100) endAngle = startAngle + val * unitValue + + context.beginPath() + context.moveTo(centerX, centerY) + context.arc(centerX, centerY, radius, startAngle, endAngle, false) + context.closePath() + context.fillStyle = this.logoColor + context.fill() + + favicon16.href = canvas.toDataURL('image/png') + favicon32.href = canvas.toDataURL('image/png') + + return + } + + // if custom favicons are set, use them + if (this.customFavicons) { + const [favicon16Path, favicon32Path] = this.customFavicons + favicon16.href = favicon16Path + favicon32.href = favicon32Path + + return + } + + // if a theme sidebar logo is set, use it + if ((this.theme?.logo?.show ?? false) && this.sidebarLogo.endsWith('.svg')) { + const response = await fetch(this.sidebarLogo) + if (!response.ok) return + + const text = await response.text() + const modifiedSvg = text.replace(/fill="var\(--color-logo, #[0-9a-fA-F]{6}\)"/g, `fill="${this.logoColor}"`) + + const blob = new Blob([modifiedSvg], { type: 'image/svg+xml' }) + const reader = new FileReader() + + reader.onloadend = () => { + const base64data = reader.result as string + favicon16.href = base64data + favicon32.href = base64data } + + reader.readAsDataURL(blob) + + return } + + // if no custom favicon is set, use the default one + const favicon = + 'data:image/svg+xml;base64,' + + window.btoa(` + + + + + + + + `) + + favicon16.href = favicon + favicon32.href = favicon } @Watch('customFavicons') diff --git a/src/components/TheConnectingDialog.vue b/src/components/TheConnectingDialog.vue index 29cad445b..d32bdd7c9 100644 --- a/src/components/TheConnectingDialog.vue +++ b/src/components/TheConnectingDialog.vue @@ -18,8 +18,7 @@

{{ $t('ConnectionDialog.CheckMoonrakerLog') }}

diff --git a/src/components/TheTopbar.vue b/src/components/TheTopbar.vue index 8fa913759..354a99fd8 100644 --- a/src/components/TheTopbar.vue +++ b/src/components/TheTopbar.vue @@ -3,7 +3,7 @@ - + Logo @@ -57,7 +57,7 @@ - + {{ $t('App.TopBar.Uploading') }} {{ uploadSnackbar.filename }}
{{ Math.round(uploadSnackbar.percent) }} % @ {{ formatFilesize(Math.round(uploadSnackbar.speed)) }}/s @@ -90,6 +90,7 @@ import { topbarHeight } from '@/store/variables' import { mdiAlertOctagonOutline, mdiContentSave, mdiFileUpload, mdiClose, mdiCloseThick } from '@mdi/js' import EmergencyStopDialog from '@/components/dialogs/EmergencyStopDialog.vue' import InlineSvg from 'vue-inline-svg' +import ThemeMixin from '@/components/mixins/theme' type uploadSnackbar = { status: boolean @@ -112,7 +113,7 @@ type uploadSnackbar = { TheNotificationMenu, }, }) -export default class TheTopbar extends Mixins(BaseMixin) { +export default class TheTopbar extends Mixins(BaseMixin, ThemeMixin) { mdiAlertOctagonOutline = mdiAlertOctagonOutline mdiContentSave = mdiContentSave mdiFileUpload = mdiFileUpload @@ -180,10 +181,6 @@ export default class TheTopbar extends Mixins(BaseMixin) { return this.$store.state.printer.hostname } - get boolWideNavDrawer() { - return this.$store.state.gui.uiSettings.boolWideNavDrawer ?? false - } - get countPrinters() { return this.$store.getters['farm/countPrinters'] } @@ -192,12 +189,8 @@ export default class TheTopbar extends Mixins(BaseMixin) { return this.$store.state.gui.uiSettings.boolHideUploadAndPrintButton ?? false } - get sidebarLogo(): string { - return this.$store.getters['files/getSidebarLogo'] - } - get isSvgLogo() { - return this.sidebarLogo.includes('.svg?timestamp=') + return this.sidebarLogo.includes('.svg?timestamp=') || this.sidebarLogo.endsWith('.svg') } get logoColor(): string { diff --git a/src/components/charts/TempChart.vue b/src/components/charts/TempChart.vue index ff472fdb7..0521918f0 100644 --- a/src/components/charts/TempChart.vue +++ b/src/components/charts/TempChart.vue @@ -350,15 +350,8 @@ export default class TempChart extends Mixins(BaseMixin, ThemeMixin) { } @Watch('selectedLegends') - selectedLegendsChanged(newVal: any, oldVal: any) { - if (this.chart?.isDisposed() !== true) { - Object.keys(newVal).forEach((key) => { - if (newVal[key] !== oldVal[key]) { - const actionType = newVal[key] ? 'legendSelect' : 'legendUnSelect' - this.chart?.dispatchAction({ type: actionType, name: key }) - } - }) - } + selectedLegendsChanged(newVal: any) { + if (this.chart?.isDisposed() !== true) this.chart?.setOption({ legend: { selected: newVal } }) } @Watch('source') diff --git a/src/components/console/ConsoleTableEntry.vue b/src/components/console/ConsoleTableEntry.vue index 9e0cd7897..21c2af4d9 100644 --- a/src/components/console/ConsoleTableEntry.vue +++ b/src/components/console/ConsoleTableEntry.vue @@ -53,6 +53,7 @@ export default class ConsoleTableEntry extends Mixins(BaseMixin) { .consoleTableRow { font-family: 'Roboto Mono', monospace; font-size: 0.95em; + white-space: pre-wrap; &.default { .col { diff --git a/src/components/dialogs/HistoryDetailsDialog.vue b/src/components/dialogs/HistoryDetailsDialog.vue deleted file mode 100644 index dfa3c2c34..000000000 --- a/src/components/dialogs/HistoryDetailsDialog.vue +++ /dev/null @@ -1,170 +0,0 @@ - - - diff --git a/src/components/dialogs/HistoryListPanelDetailsDialog.vue b/src/components/dialogs/HistoryListPanelDetailsDialog.vue index 16f7f33f8..2a8138ac5 100644 --- a/src/components/dialogs/HistoryListPanelDetailsDialog.vue +++ b/src/components/dialogs/HistoryListPanelDetailsDialog.vue @@ -104,8 +104,69 @@ export default class HistoryListPanelDetailsDialog extends Mixins(BaseMixin) { value: `${Math.round((this.job.metadata?.filament_weight_total ?? 0) * 100) / 100} g`, exists: this.job.metadata && 'filament_weight_total' in this.job.metadata, }, + { + name: this.$t('History.EstimatedFilament').toString(), + value: `${Math.round(this.job.metadata?.filament_total ?? 0)} mm`, + exists: this.job.metadata && 'filament_total' in this.job.metadata, + }, + { + name: this.$t('History.FilamentUsed').toString(), + value: `${Math.round(this.job.metadata?.filament_used ?? 0)} mm`, + exists: this.job.metadata && 'filament_used' in this.job.metadata, + }, + { + name: this.$t('History.FirstLayerExtTemp').toString(), + value: `${this.job.metadata?.first_layer_extr_temp ?? 0} °C`, + exists: this.job.metadata && 'first_layer_extr_temp' in this.job.metadata, + }, + { + name: this.$t('History.FirstLayerBedTemp').toString(), + value: `${this.job.metadata?.first_layer_bed_temp ?? 0} °C`, + exists: this.job.metadata && 'first_layer_bed_temp' in this.job.metadata, + }, + { + name: this.$t('History.FirstLayerHeight').toString(), + value: `${this.job.metadata?.first_layer_height ?? 0} mm`, + exists: this.job.metadata && 'first_layer_height' in this.job.metadata, + }, + { + name: this.$t('History.LayerHeight').toString(), + value: `${this.job.metadata?.layer_height ?? 0} mm`, + exists: this.job.metadata && 'layer_height' in this.job.metadata, + }, + { + name: this.$t('History.ObjectHeight').toString(), + value: `${this.job.metadata?.object_height ?? 0} mm`, + exists: this.job.metadata && 'object_height' in this.job.metadata, + }, + { + name: this.$t('History.Slicer').toString(), + value: this.job.metadata?.slicer ?? '--', + exists: this.job.metadata && 'slicer' in this.job.metadata, + }, + { + name: this.$t('History.SlicerVersion').toString(), + value: this.job.metadata?.slicer_version ?? '--', + exists: this.job.metadata && 'slicer_version' in this.job.metadata, + }, ] + if ('auxiliary_data' in this.job) { + this.job.auxiliary_data?.forEach((data) => { + let value = data.value.toString() + if (!Array.isArray(data.value)) { + value = `${Math.round(data.value * 1000) / 1000} ${data.units}` + } + if (value === '') value = '--' + + entries.push({ + name: data.description, + value, + exists: true, + }) + }) + } + return entries.filter((entry) => entry.exists) } diff --git a/src/components/dialogs/TheMacroPrompt.vue b/src/components/dialogs/TheMacroPrompt.vue index 68356dbc3..171723fbc 100644 --- a/src/components/dialogs/TheMacroPrompt.vue +++ b/src/components/dialogs/TheMacroPrompt.vue @@ -100,7 +100,13 @@ export default class TheMacroPrompt extends Mixins(BaseMixin) { get showDialog() { if (this.lastPromptBeginPos === -1) return false - if (this.internalCloseCommand !== null && this.internalCloseCommand == this.lastPromptBeginPos) return false + + const lastBeginEvent = this.macroPromptEvents[this.lastPromptBeginPos] ?? null + if ( + this.internalCloseCommand !== null && + this.internalCloseCommand == (lastBeginEvent?.date?.getTime() ?? null) + ) + return false return this.lastPromptBeginPos > this.lastPromptClosePos && this.activePromptContent.length > 0 } @@ -152,7 +158,7 @@ export default class TheMacroPrompt extends Mixins(BaseMixin) { closePrompt() { // close prompt immediately, because klipper could be busy - this.internalCloseCommand = this.lastPromptBeginPos + this.internalCloseCommand = this.macroPromptEvents[this.lastPromptBeginPos]?.date?.getTime() ?? null const gcode = `RESPOND type="command" msg="action:prompt_end"` this.$store.dispatch('server/addEvent', { message: gcode, type: 'command' }) diff --git a/src/components/dialogs/TheScrewsTiltAdjustDialog.vue b/src/components/dialogs/TheScrewsTiltAdjustDialog.vue index fb16ba235..038ca115d 100644 --- a/src/components/dialogs/TheScrewsTiltAdjustDialog.vue +++ b/src/components/dialogs/TheScrewsTiltAdjustDialog.vue @@ -50,6 +50,7 @@ import SettingsRow from '@/components/settings/SettingsRow.vue' import { mdiArrowCollapseDown, mdiCloseThick } from '@mdi/js' import ControlMixin from '@/components/mixins/control' import TheScrewsTiltAdjustDialogEntry from '@/components/dialogs/TheScrewsTiltAdjustDialogEntry.vue' +import { ServerStateEvent } from '@/store/server/types' @Component({ components: { TheScrewsTiltAdjustDialogEntry, Panel, Responsive, SettingsRow }, }) @@ -92,9 +93,17 @@ export default class TheScrewsTiltAdjustDialog extends Mixins(BaseMixin, Control } async retryScrewsTiltAdjust() { + const entries = [...(this.$store.state.server.events ?? [])] + const lastCommand = entries + .reverse() + .find( + (entry: ServerStateEvent) => + entry.type === 'command' && entry.message.startsWith('SCREWS_TILT_CALCULATE') + ) + await this.$store.dispatch('printer/clearScrewsTiltAdjust') - this.doSend('SCREWS_TILT_CALCULATE') + this.doSend(lastCommand?.message ?? 'SCREWS_TILT_CALCULATE') } } diff --git a/src/components/mixins/history.ts b/src/components/mixins/history.ts new file mode 100644 index 000000000..80b763aac --- /dev/null +++ b/src/components/mixins/history.ts @@ -0,0 +1,31 @@ +import Vue from 'vue' +import Component from 'vue-class-component' + +@Component +export default class HistoryMixin extends Vue { + get moonrakerHistoryFields() { + const config = this.$store.state.server.config?.config ?? {} + const sensors = Object.keys(config).filter((key) => key.startsWith('sensor ')) + const historyFields: { desc: string; unit: string; provider: string; name: string; parameter: string }[] = [] + + sensors.forEach((configName) => { + const sensor = config[configName] ?? {} + + Object.keys(sensor) + .filter((key) => key.startsWith('history_field_')) + .forEach((key) => { + const historyField = sensor[key] + + historyFields.push({ + desc: historyField.desc, + unit: historyField.units, + provider: configName, + parameter: historyField.parameter, + name: key, + }) + }) + }) + + return historyFields + } +} diff --git a/src/components/mixins/theme.ts b/src/components/mixins/theme.ts index fee4bfb5b..b3f612a8a 100644 --- a/src/components/mixins/theme.ts +++ b/src/components/mixins/theme.ts @@ -12,6 +12,18 @@ export default class ThemeMixin extends Vue { return this.fgColor(alpha, !this.$vuetify.theme.dark) } + get themeName() { + return this.$store.getters['gui/theme'] + } + + get theme() { + return this.$store.getters['gui/getTheme'] + } + + get themeMode() { + return this.$store.state.gui.uiSettings.mode ?? 'dark' + } + get fgColorHi() { return this.fgColor(0.8) } @@ -42,6 +54,40 @@ export default class ThemeMixin extends Vue { } get sidebarBgImage() { + if (this.theme.sidebarBackground?.show) { + if (this.theme.sidebarBackground?.light && this.themeMode === 'light') + return `/img/themes/sidebarBackground-${this.themeName}-light.png` + + return `/img/themes/sidebarBackground-${this.themeName}.png` + } + return this.$vuetify.theme.dark ? '/img/sidebar-background.svg' : '/img/sidebar-background-light.svg' } + + get sidebarLogo(): string { + const url = this.$store.getters['files/getSidebarLogo'] + if (url !== '' || this.themeName === 'mainsail') return url + + // if no theme is set, return empty string to load the default logo + if (!(this.theme.logo?.show ?? false)) return '' + + // return light logo if theme is light and sidebarLogo is set to both + if (this.theme.logo?.light && this.themeMode === 'light') + return `/img/themes/sidebarLogo-${this.themeName}-light.svg` + + // return dark/generic theme logo + return `/img/themes/sidebarLogo-${this.themeName}.svg` + } + + get mainBgImage() { + const url = this.$store.getters['files/getMainBackground'] + if (url || this.themeName === 'mainsail') return url + + if (!this.theme.mainBackground?.show) return null + + if (this.theme.mainBackground?.light && this.themeMode === 'light') + return `/img/themes/mainBackground-${this.themeName}-light.png` + + return `/img/themes/mainBackground-${this.themeName}.png` + } } diff --git a/src/components/panels/Extruder/EstimatedExtrusionOutput.vue b/src/components/panels/Extruder/EstimatedExtrusionOutput.vue index 6d252ed84..2ce18edae 100644 --- a/src/components/panels/Extruder/EstimatedExtrusionOutput.vue +++ b/src/components/panels/Extruder/EstimatedExtrusionOutput.vue @@ -8,6 +8,21 @@ {{ mdiDiameterVariant }} {{ nozzleDiameter }} mm + + + +
+ {{ $t('Panels.ToolheadControlPanel.SpeedFactor') }}: {{ speed_factor * 100 }} % +
+
+ {{ $t('Panels.ExtruderControlPanel.ExtrusionFactor') }}: {{ extrudeFactor * 100 }} % +
+
+
@@ -16,12 +31,13 @@ diff --git a/src/components/panels/History/HistoryListEntryJob.vue b/src/components/panels/History/HistoryListEntryJob.vue index 29cd970b6..aa8da85d9 100644 --- a/src/components/panels/History/HistoryListEntryJob.vue +++ b/src/components/panels/History/HistoryListEntryJob.vue @@ -300,6 +300,11 @@ export default class HistoryListPanel extends Mixins(BaseMixin) { //@ts-ignore let value = col.value in item ? item[col.value] : null if (value === null) value = col.value in item.metadata ? item.metadata[col.value] : null + if (col.value.startsWith('history_field_')) { + const fieldName = col.value.replace('history_field_', '') + const field = item.auxiliary_data?.find((field: any) => field.name === fieldName) + if (field && !Array.isArray(field.value)) return `${Math.round(field.value * 1000) / 1000} ${field.units}` + } if (value === null) return '--' if (col.value === 'slicer') value += '
' + item.metadata.slicer_version diff --git a/src/components/panels/History/HistoryListRow.vue b/src/components/panels/History/HistoryListRow.vue deleted file mode 100644 index 93214a4c6..000000000 --- a/src/components/panels/History/HistoryListRow.vue +++ /dev/null @@ -1,226 +0,0 @@ - - diff --git a/src/components/panels/History/HistoryListRowCell.vue b/src/components/panels/History/HistoryListRowCell.vue deleted file mode 100644 index c651b08a0..000000000 --- a/src/components/panels/History/HistoryListRowCell.vue +++ /dev/null @@ -1,61 +0,0 @@ - - diff --git a/src/components/panels/HistoryListPanel.vue b/src/components/panels/HistoryListPanel.vue index 99aff2093..85b2b404f 100644 --- a/src/components/panels/HistoryListPanel.vue +++ b/src/components/panels/HistoryListPanel.vue @@ -178,6 +178,7 @@ import HistoryListPanelAddMaintenance from '@/components/dialogs/HistoryListPane import { GuiMaintenanceStateEntry, HistoryListRowMaintenance } from '@/store/gui/maintenance/types' import HistoryListEntryMaintenance from '@/components/panels/History/HistoryListEntryMaintenance.vue' import HistoryListPanelDeleteSelectedDialog from '@/components/dialogs/HistoryListPanelDeleteSelectedDialog.vue' +import HistoryMixin from '@/components/mixins/history' export type HistoryListPanelRow = HistoryListRowJob | HistoryListRowMaintenance @@ -201,7 +202,7 @@ export interface HistoryListPanelCol { Panel, }, }) -export default class HistoryListPanel extends Mixins(BaseMixin) { +export default class HistoryListPanel extends Mixins(BaseMixin, HistoryMixin) { mdiCloseThick = mdiCloseThick mdiCog = mdiCog mdiDatabaseArrowDownOutline = mdiDatabaseArrowDownOutline @@ -409,6 +410,16 @@ export default class HistoryListPanel extends Mixins(BaseMixin) { }, ] + this.moonrakerHistoryFields.forEach((sensor) => { + headers.push({ + text: sensor.desc, + value: sensor.name, + align: 'left', + configable: true, + visible: false, + }) + }) + headers.forEach((header) => { if (header.visible && this.hideColums.includes(header.value)) { header.visible = false @@ -544,6 +555,12 @@ export default class HistoryListPanel extends Mixins(BaseMixin) { row.push('status') this.tableFields.forEach((col) => { + if (col.value.startsWith('history_field_')) { + const sensorName = col.value.replace('history_field_', '') + row.push(sensorName) + return + } + row.push(col.value) }) @@ -612,18 +629,9 @@ export default class HistoryListPanel extends Mixins(BaseMixin) { row.push('job') row.push(job.status) - this.tableFields - .filter((header) => header.value !== 'slicer') - .forEach((col) => { - row.push(this.outputValue(col, job, csvSeperator)) - }) - - if (this.tableFields.find((header) => header.value === 'slicer')?.visible) { - let slicerString = 'slicer' in job.metadata && job.metadata.slicer ? job.metadata.slicer : '--' - if ('slicer_version' in job.metadata && job.metadata.slicer_version) - slicerString += ' ' + job.metadata.slicer_version - row.push(slicerString) - } + this.tableFields.forEach((col) => { + row.push(this.outputValue(col, job, csvSeperator)) + }) content.push(row) }) @@ -634,7 +642,7 @@ export default class HistoryListPanel extends Mixins(BaseMixin) { const csvContent = 'data:text/csv;charset=utf-8,' + content.map((entry) => - entry.map((field) => (field.indexOf(csvSeperator) === -1 ? field : `"${field}"`)).join(csvSeperator) + entry.map((field) => (field?.indexOf(csvSeperator) === -1 ? field : `"${field}"`)).join(csvSeperator) ).join('\n') const link = document.createElement('a') @@ -651,6 +659,36 @@ export default class HistoryListPanel extends Mixins(BaseMixin) { let value = col.value in job ? job[col.value] : null if (value === null) value = col.value in job.metadata ? job.metadata[col.value] : null + if (col.value === 'slicer') { + let slicerString = 'slicer' in job.metadata && job.metadata.slicer ? job.metadata.slicer : '--' + if ('slicer_version' in job.metadata && job.metadata.slicer_version) + slicerString += ' ' + job.metadata.slicer_version + + if (csvSeperator !== null && value.includes(csvSeperator)) return '"' + slicerString + '"' + + return slicerString + } + + if (col.value.startsWith('history_field_')) { + const sensorName = col.value.replace('history_field_', '') + const sensor = job.auxiliary_data?.find((sensor) => sensor.name === sensorName) + + let value = sensor?.value?.toString() + + // return value, when it is not an array + if (sensor && !Array.isArray(sensor.value)) { + value = sensor.value?.toLocaleString(this.browserLocale, { useGrouping: false }) ?? 0 + } + + // return empty string, when value is null + if (!value) return '--' + + // escape fields with the csvSeperator in the content + if (csvSeperator !== null && value?.includes(csvSeperator)) return `"${value}"` + + return value + } + switch (col.outputType) { case 'date': return this.formatDateTime(value * 1000) diff --git a/src/components/panels/HistoryStatisticsPanel.vue b/src/components/panels/HistoryStatisticsPanel.vue index 628bb12c6..a2099cca2 100644 --- a/src/components/panels/HistoryStatisticsPanel.vue +++ b/src/components/panels/HistoryStatisticsPanel.vue @@ -9,50 +9,10 @@ - - + + {{ total.title }} + {{ total.value }} + @@ -104,13 +64,14 @@ import Panel from '@/components/ui/Panel.vue' import HistoryFilamentUsage from '@/components/charts/HistoryFilamentUsage.vue' import HistoryPrinttimeAvg from '@/components/charts/HistoryPrinttimeAvg.vue' import HistoryAllPrintStatusChart from '@/components/charts/HistoryAllPrintStatusChart.vue' -import { ServerHistoryStateJob } from '@/store/server/history/types' +import { ServerHistoryStateJob, ServerHistoryStateJobAuxiliaryTotal } from '@/store/server/history/types' import { mdiChartAreaspline, mdiDatabaseArrowDownOutline } from '@mdi/js' import { formatPrintTime } from '@/plugins/helpers' +import HistoryMixin from '@/components/mixins/history' @Component({ components: { Panel, HistoryFilamentUsage, HistoryPrinttimeAvg, HistoryAllPrintStatusChart }, }) -export default class HistoryStatisticsPanel extends Mixins(BaseMixin) { +export default class HistoryStatisticsPanel extends Mixins(BaseMixin, HistoryMixin) { mdiChartAreaspline = mdiChartAreaspline mdiDatabaseArrowDownOutline = mdiDatabaseArrowDownOutline formatPrintTime = formatPrintTime @@ -215,6 +176,112 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) { return this.$store.state.server.history.all_loaded ?? false } + get selectedTotals() { + const output: { title: string; value: string }[] = [ + { + title: this.$t('History.SelectedPrinttime') as string, + value: this.formatPrintTime(this.selectedPrintTime, false), + }, + { + title: this.$t('History.LongestPrinttime') as string, + value: this.formatPrintTime(this.selectedLongestPrintTime, false), + }, + { + title: this.$t('History.AvgPrinttime') as string, + value: this.formatPrintTime(this.selectedAvgPrintTime, false), + }, + { + title: this.$t('History.SelectedFilamentUsed') as string, + value: this.selectedFilamentUsedFormat, + }, + { + title: this.$t('History.SelectedJobs') as string, + value: this.selectedJobs.length.toString(), + }, + ] + + output.push(...this.auxiliarySelectedTotals) + + return output + } + + get auxiliarySelectedTotals() { + const output: { title: string; value: string }[] = [] + this.moonrakerHistoryFields.forEach((historyField) => { + const value = this.selectedJobs.reduce((acc: number, job: ServerHistoryStateJob) => { + const historyFieldName = historyField.name.replace('history_field_', '') + const auxiliary_data = job.auxiliary_data?.find( + (auxiliary) => auxiliary.provider === historyField.provider && auxiliary.name === historyFieldName + ) + + if (!auxiliary_data || typeof auxiliary_data.value !== 'number') return acc + + return acc + auxiliary_data.value + }, 0) + + output.push({ + title: historyField.desc, + value: `${Math.round(value * 1000) / 1000} ${historyField.unit}`, + }) + }) + + return output + } + + get genericTotals() { + const output: { title: string; value: string }[] = [ + { + title: this.$t('History.TotalPrinttime') as string, + value: this.formatPrintTime(this.totalPrintTime, false), + }, + { + title: this.$t('History.LongestPrinttime') as string, + value: this.formatPrintTime(this.longestPrintTime, false), + }, + { + title: this.$t('History.AvgPrinttime') as string, + value: this.formatPrintTime(this.avgPrintTime, false), + }, + { + title: this.$t('History.TotalFilamentUsed') as string, + value: this.totalFilamentUsedFormat, + }, + { + title: this.$t('History.TotalJobs') as string, + value: this.totalJobsCount.toString(), + }, + ] + + // Add auxiliary totals + output.push(...this.auxiliaryTotals) + + return output + } + + get auxiliaryTotals() { + const auxiliaries = this.$store.state.server.history.auxiliary_totals ?? [] + const output: { title: string; value: string }[] = [] + + auxiliaries.forEach((auxiliary: ServerHistoryStateJobAuxiliaryTotal) => { + const historyFieldName = `history_field_${auxiliary.field}` + const historyField = this.moonrakerHistoryFields.find( + (historyField) => historyField.provider === auxiliary.provider && historyField.name === historyFieldName + ) + const value = Math.round((auxiliary.total ?? 0) * 1000) / 1000 + + output.push({ + title: historyField?.desc ?? auxiliary.field, + value: `${value} ${historyField?.unit}`, + }) + }) + + return output + } + + get totals() { + return this.existsSelectedJobs ? this.selectedTotals : this.genericTotals + } + refreshHistory() { this.$store.dispatch('socket/addLoading', { name: 'historyLoadAll' }) diff --git a/src/components/panels/Machine/UpdatePanel/Entry.vue b/src/components/panels/Machine/UpdatePanel/Entry.vue index 32c43c0c7..5da1baf28 100644 --- a/src/components/panels/Machine/UpdatePanel/Entry.vue +++ b/src/components/panels/Machine/UpdatePanel/Entry.vue @@ -278,19 +278,20 @@ export default class UpdatePanelEntry extends Mixins(BaseMixin) { if (['printing', 'paused'].includes(this.printer_state)) return true if (!this.isValid || this.isCorrupt || this.isDirty || this.commitsBehind.length) return false - return !(this.localVersion && this.remoteVersion && semver.gt(this.remoteVersion, this.localVersion)) + if (this.type === 'web') return !this.webUpdatable + + return this.commitsBehind.length === 0 } get btnIcon() { if (this.isDetached || !this.isValid || this.isCorrupt || this.isDirty) return mdiCloseCircle - if ( - this.commitsBehind.length || - (this.localVersion && this.remoteVersion && semver.gt(this.remoteVersion, this.localVersion)) - ) - return mdiProgressUpload + if (this.type === 'web') { + if (this.webUpdatable) return mdiProgressUpload + else if (this.localVersion === null || this.remoteVersion === null) return mdiHelpCircleOutline + } - if (this.localVersion === null || this.remoteVersion === null) return mdiHelpCircleOutline + if (this.type === 'git_repo' && this.commitsBehind.length) return mdiProgressUpload return mdiCheck } @@ -298,11 +299,8 @@ export default class UpdatePanelEntry extends Mixins(BaseMixin) { get btnColor() { if (this.isCorrupt || this.isDetached || this.isDirty || !this.isValid) return 'orange' - if ( - this.commitsBehind.length || - (this.localVersion && this.remoteVersion && semver.gt(this.remoteVersion, this.localVersion)) - ) - return 'primary' + if (this.type === 'web' && this.webUpdatable) return 'primary' + if (this.type === 'git_repo' && this.commitsBehind.length) return 'primary' return 'green' } @@ -312,13 +310,14 @@ export default class UpdatePanelEntry extends Mixins(BaseMixin) { if (this.isDetached) return this.$t('Machine.UpdatePanel.Detached') if (this.isDirty) return this.$t('Machine.UpdatePanel.Dirty') if (!this.isValid) return this.$t('Machine.UpdatePanel.Invalid') - if ( - this.commitsBehind.length || - (this.localVersion && this.remoteVersion && semver.gt(this.remoteVersion, this.localVersion)) - ) - return this.$t('Machine.UpdatePanel.Update') - if (this.localVersion === null || this.remoteVersion === null) return this.$t('Machine.UpdatePanel.Unknown') + if (this.type === 'web') { + if (this.webUpdatable) return this.$t('Machine.UpdatePanel.Update') + else if (this.localVersion === null || this.remoteVersion === null) + return this.$t('Machine.UpdatePanel.Unknown') + } + + if (this.type === 'git_repo' && this.commitsBehind.length) return this.$t('Machine.UpdatePanel.Update') return this.$t('Machine.UpdatePanel.UpToDate') } diff --git a/src/components/panels/Miscellaneous/MoonrakerSensor.vue b/src/components/panels/Miscellaneous/MoonrakerSensor.vue new file mode 100644 index 000000000..e0b919518 --- /dev/null +++ b/src/components/panels/Miscellaneous/MoonrakerSensor.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/panels/Miscellaneous/MoonrakerSensorValue.vue b/src/components/panels/Miscellaneous/MoonrakerSensorValue.vue new file mode 100644 index 000000000..1ac9f31c2 --- /dev/null +++ b/src/components/panels/Miscellaneous/MoonrakerSensorValue.vue @@ -0,0 +1,92 @@ + + + diff --git a/src/components/panels/MiscellaneousPanel.vue b/src/components/panels/MiscellaneousPanel.vue index 5f12ca144..8afac842c 100644 --- a/src/components/panels/MiscellaneousPanel.vue +++ b/src/components/panels/MiscellaneousPanel.vue @@ -8,7 +8,7 @@ :collapsible="true" card-class="miscellaneous-panel">
- +
- +
- + + :type="sensor.type" /> +
+
+ +
@@ -56,11 +59,12 @@ import BaseMixin from '@/components/mixins/base' import MiscellaneousSlider from '@/components/inputs/MiscellaneousSlider.vue' import MiscellaneousLight from '@/components/inputs/MiscellaneousLight.vue' import FilamentSensor from '@/components/inputs/FilamentSensor.vue' +import MoonrakerSensor from '@/components/panels/Miscellaneous/MoonrakerSensor.vue' import Panel from '@/components/ui/Panel.vue' import { mdiDipSwitch } from '@mdi/js' import {PrinterStateLight} from "@/store/printer/types"; @Component({ - components: { Panel, FilamentSensor, MiscellaneousSlider, MiscellaneousLight }, + components: { Panel, FilamentSensor, MiscellaneousSlider, MiscellaneousLight, MoonrakerSensor }, }) export default class MiscellaneousPanel extends Mixins(BaseMixin) { mdiDipSwitch = mdiDipSwitch @@ -77,6 +81,10 @@ export default class MiscellaneousPanel extends Mixins(BaseMixin) { return this.$store.getters['printer/getLights'] ?? [] } + get moonrakerSensors() { + return this.$store.getters['server/sensor/getSensors'] ?? [] + } + get showMiscellaneousPanel() { return ( this.klipperReadyForGui && (this.miscellaneous.length || this.filamentSensors.length || this.lights.length) diff --git a/src/components/panels/Status/PrintstatusPrinting.vue b/src/components/panels/Status/PrintstatusPrinting.vue index 0333a58eb..21b2ea512 100644 --- a/src/components/panels/Status/PrintstatusPrinting.vue +++ b/src/components/panels/Status/PrintstatusPrinting.vue @@ -239,13 +239,13 @@ export default class StatusPanelPrintstatusPrinting extends Mixins(BaseMixin) { } formatDuration(seconds: number) { - let prefix = seconds < 0 ? '-' : '' + const prefix = seconds < 0 ? '-' : '' let absSeconds = Math.abs(seconds) - let h = Math.floor(absSeconds / 3600) + const h = Math.floor(absSeconds / 3600) absSeconds %= 3600 - let m = ('0' + Math.floor(absSeconds / 60)).slice(-2) - let s = ('0' + (absSeconds % 60).toFixed(0)).slice(-2) + const m = ('0' + Math.floor(absSeconds / 60)).slice(-2) + const s = ('0' + Math.floor(absSeconds % 60)).slice(-2) return prefix + h + ':' + m + ':' + s } diff --git a/src/components/panels/Status/PrintstatusThumbnail.vue b/src/components/panels/Status/PrintstatusThumbnail.vue index c62cca33c..bcabd19f6 100644 --- a/src/components/panels/Status/PrintstatusThumbnail.vue +++ b/src/components/panels/Status/PrintstatusThumbnail.vue @@ -10,7 +10,7 @@ :style="thumbnailStyle" @focus="focus = true" @blur="focus = false"> - + @@ -208,6 +208,19 @@ export default class StatusPanelPrintstatusThumbnail extends Mixins(BaseMixin) { return output } + get styleThumbnailOverlay() { + const style = { + backgroundColor: 'rgba(0, 0, 0, 0.3)', + backdropFilter: 'blur(3px)', + } + + if (!this.$vuetify.theme.dark) { + style.backgroundColor = 'rgba(255, 255, 255, 0.3)' + } + + return style + } + get thumbnailBlurHeight() { if (this.thumbnailFactor === 0) return 0 diff --git a/src/components/settings/SettingsTimelapseTab.vue b/src/components/settings/SettingsTimelapseTab.vue index 13cc39b70..b39f84d27 100644 --- a/src/components/settings/SettingsTimelapseTab.vue +++ b/src/components/settings/SettingsTimelapseTab.vue @@ -27,14 +27,19 @@ + :sub-title="$t('Settings.TimelapseTab.CameraDescriptionWithSnapshotUrl')"> + + {{ $t('Settings.TimelapseTab.CameraWarningAlreadySet') }} + ({{ $t('Settings.TimelapseTab.CameraWarningAlreadySetSmall') }}) + + :disabled="blockedsettings.includes('camera') || availableSnapshotWebcams.length === 0" /> webcam.snapshot_url !== '' + ) + } + get cameraOptions() { - const webcams = this.$store.getters['gui/webcams/getWebcams'] - const output: any = [] - - webcams - .filter((webcam: GuiWebcamStateWebcam) => webcam.snapshot_url !== '') - .forEach((webcam: GuiWebcamStateWebcam) => { - output.push({ - text: webcam.name, - value: webcam.name, - }) + let output: { text: string | TranslateResult; value: string | null }[] = [] + + if (this.availableSnapshotWebcams.length === 0) { + return [{ value: null, text: this.$t('Settings.TimelapseTab.NoWebcamFound') }] + } + + this.availableSnapshotWebcams.forEach((webcam: GuiWebcamStateWebcam) => { + output.push({ + text: webcam.name, + value: webcam.name, }) + }) - return caseInsensitiveSort(output, 'text') + output = caseInsensitiveSort(output, 'text') + + if (this.camera === null) { + output.unshift({ value: null, text: this.$t('Settings.TimelapseTab.SelectWebcam') }) + } + + return output } get blockedsettings() { @@ -853,6 +872,17 @@ export default class SettingsTimelapseTab extends Mixins(BaseMixin) { } get camera() { + const value = this.$store.state.server.timelapse.settings.camera ?? null + + if ( + value === null || + this.blockedsettings.includes('snapshoturl') || + this.availableSnapshotWebcams.length === 0 || + this.availableSnapshotWebcams.find((webcam) => webcam.name === value) === undefined + ) { + return null + } + return this.$store.state.server.timelapse.settings.camera } diff --git a/src/components/settings/SettingsUiSettingsTab.vue b/src/components/settings/SettingsUiSettingsTab.vue index cbc4122ef..70c9171d4 100644 --- a/src/components/settings/SettingsUiSettingsTab.vue +++ b/src/components/settings/SettingsUiSettingsTab.vue @@ -2,10 +2,16 @@
+ + + + - + @@ -297,34 +303,49 @@ diff --git a/src/components/webcams/streamers/Mjpegstreamer.vue b/src/components/webcams/streamers/Mjpegstreamer.vue index 89f2a887c..d42ecf78d 100644 --- a/src/components/webcams/streamers/Mjpegstreamer.vue +++ b/src/components/webcams/streamers/Mjpegstreamer.vue @@ -257,4 +257,8 @@ export default class Mjpegstreamer extends Mixins(BaseMixin, WebcamMixin) { padding: 3px 10px; border-top-left-radius: 5px; } + +html.theme--light .webcamFpsOutput { + background: rgba(255, 255, 255, 0.7); +} diff --git a/src/components/webcams/streamers/MjpegstreamerAdaptive.vue b/src/components/webcams/streamers/MjpegstreamerAdaptive.vue index 32d7d2f97..00b7aaa11 100644 --- a/src/components/webcams/streamers/MjpegstreamerAdaptive.vue +++ b/src/components/webcams/streamers/MjpegstreamerAdaptive.vue @@ -226,4 +226,8 @@ export default class MjpegstreamerAdaptive extends Mixins(BaseMixin, WebcamMixin padding: 3px 10px; border-top-left-radius: 5px; } + +html.theme--light .webcamFpsOutput { + background: rgba(255, 255, 255, 0.7); +} diff --git a/src/locales/de.json b/src/locales/de.json index 261990a77..71112b2f5 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -35,7 +35,11 @@ "OneHourShort": "1Std", "OneWeekShort": "1W", "Remind": "Erinnere:", - "ShowDetails": "Details anzeigen" + "ShowDetails": "Details anzeigen", + "TmcOtFlag": "Stepper-Treiber Fehler: OT-Flag ist gesetzt", + "TmcOtFlagText": "Der Stepper-Treiber '{Name}' hat das OT-Flag ausgelöst und funktioniert nicht mehr. Dies kann durch einen zu hohen Strom verursacht werden. Bitte überprüfe die Einstellungen und die Kühlung des Steppertreibers.", + "TmcOtpwFlag": "Stepper-Treiber Fehler: OTPW-Flag ist gesetzt", + "TmcOtpwFlagText": "Der Stepper-Treiber '{Name}' hat das OTPW-Flag ausgelöst und funktioniert möglicherweise nicht mehr, wenn er noch heißer wird. Dies ist ein Hinweis auf einen Überhitzungszustand. Dies kann durch einen zu hohen Strom verursacht werden. Bitte überprüfe die Einstellungen und die Kühlung des Steppertreibers." }, "NumberInput": { "GreaterOrEqualError": "Muss größer oder gleich {min} sein!", @@ -371,7 +375,7 @@ "EntryNextPerform": "Nächste Ausführung:", "EntryPerformedAt": "Ausgeführt am {date}.", "EntrySince": "Verwendet seit:", - "EstimatedFilament": "Geschätztes Filament", + "EstimatedFilament": "Geschätzte Filamentlänge", "EstimatedFilamentWeight": "Geschätztes Filamentgewicht", "EstimatedTime": "Geschätzte Zeit", "FilamentBasedReminder": "Filament", @@ -422,7 +426,7 @@ "SelectedJobs": "Ausg. Drucke", "SelectedPrinttime": "Ausg. Druckzeit", "Slicer": "Slicer", - "SlicerVersion": "Slicer Version", + "SlicerVersion": "Slicerversion", "StartTime": "Startzeit", "Statistics": "Statistik", "Status": "Status", @@ -1114,7 +1118,9 @@ "Autorender": "Autorender", "AutorenderDescription": "Wenn diese Option aktiviert ist, wird das Zeitraffervideo am Ende des Druckvorgangs automatisch gerendert", "Camera": "Kamera", - "CameraDescription": "Legt fest, welche Kamera verwendet werden soll", + "CameraDescriptionWithSnapshotUrl": "Wähle aus, welche Kamera (mit Schnappschuss URL) verwendet werden soll.", + "CameraWarningAlreadySet": "Dieser Wert ist bereits in der Moonraker-Konfigurationsdatei festgelegt.", + "CameraWarningAlreadySetSmall": "snapshoturl ist in der [timelapse] Sektion", "ConstantRateFactor": "Constant Rate Factor", "ConstantRateFactorDescription": "Damit wird die Qualität im Verhältnis zur Dateigröße des gerenderten Videos konfiguriert. Die CRF-Skala reicht von 0-51, wobei 0 für verlustfrei, 23 für die Standardeinstellung und 51 für die schlechtestmögliche Qualität steht. Ein niedrigerer Wert führt im Allgemeinen zu höherer Qualität, und ein subjektiv vernünftiger Bereich ist 17-28. 17 oder 18 gelten als visuell verlustfrei.", "duplicatelastframe": "Letztes Bild duplizieren", @@ -1132,6 +1138,7 @@ "HyperlapseCycleDescription": "Es wird alle X Sekunden ein Schnappschuss gemacht", "Mode": "Modus", "ModeDescription": "Wähle zwischen Layermacro und Hyperlapse (zeitbasiert) Modus", + "NoWebcamFound": "Keine Webcam verfügbar", "OutputFramerate": "Ausgabe Bildrate", "OutputFramerateDescription": "Bestimmt die Framerate des Videos. Hinweis: Dies wird ignoriert, wenn variable_fps aktiviert ist", "Parkhead": "Druckkopf parken", @@ -1162,6 +1169,7 @@ "RulesZeroAndPositive": "Der Wert muss 0 oder größer sein!", "SaveFrames": "Bilder speichern", "SaveFramesDescription": "Speichern der Bilder in einer Zip-Datei für externes Rendern", + "SelectWebcam": "Webcam wählen...", "StreamDelayCompensation": "Stream-Verzögerungs-Kompensation", "StreamDelayCompensationDescription": "Verzögern der Bildaufnahme", "Targetlength": "Zieldauer", @@ -1223,6 +1231,8 @@ "Logo": "Logo", "ManualProbeDialog": "Hilfsfenster für manuelle Messung", "ManualProbeDialogDescription": "Zeige ein Hilfsfenster für PROBE_CALIBRATE oder Z_ENDSTOP_CALIBRATE an.", + "Mode": "Modus", + "ModeDescription": "Ändert das allgemeine Aussehen der Anwendung.", "NavigationStyle": "Stil der Navigation", "NavigationStyleDescription": "Erscheinungsbild der Navigation ändern", "NavigationStyleIconsAndText": "Icons + Text", @@ -1230,6 +1240,8 @@ "PowerDeviceName": "Stromversorgung für Drucker", "PowerDeviceNameDescription": "Wähle aus, welches Moonraker Power-Device zum Einschalten des Druckers verwendet werden soll.", "Primary": "Primärfarbe", + "PrintstatusThumbnailZoom": "Großes-Vorschaubild Vergrößerung", + "PrintstatusThumbnailZoomDescription": "Dadurch wird der Vergrößerungseffekt des Vorschaubildes im Statusfenster deaktiviert.", "ProgressAsFavicon": "Fortschritt als Favicon anzeigen", "ProgressAsFaviconDescription": "Ändere das Mainsail-Logo-Favicon in einen Fortschrittskreis.", "ScrewsTiltAdjustDialog": "Hilfsfenster für Schrauben Neigunganpassung", diff --git a/src/locales/en.json b/src/locales/en.json index 47464d100..2ad1066f1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -35,7 +35,11 @@ "OneHourShort": "1H", "OneWeekShort": "1W", "Remind": "Remind:", - "ShowDetails": "show details" + "ShowDetails": "show details", + "TmcOtFlag": "Stepper driver error: OT flag set", + "TmcOtFlagText": "The stepper driver '{name}' has triggered the OT flag and stopped working. This can be caused by a too high current. Please check the stepper driver settings and cooling.", + "TmcOtpwFlag": "Stepper driver warning: OTPW flag set", + "TmcOtpwFlagText": "The stepper driver '{name}' has triggered the OTPW flag and may stop working if it gets any hotter. This is an indication of an over temperature condition. This can be caused by a too high current. Please check the stepper driver settings and cooling." }, "NumberInput": { "GreaterOrEqualError": "Must be greater or equal than {min}!", @@ -1183,7 +1187,9 @@ "Autorender": "Autorender", "AutorenderDescription": "If enabled, the timelapse video will automatically render at the end of the print", "Camera": "Camera", - "CameraDescription": "Select which camera should be used", + "CameraDescriptionWithSnapshotUrl": "Select which camera (with snapshot URL) should be used", + "CameraWarningAlreadySet": "This value is already set in the Moonraker configuration file.", + "CameraWarningAlreadySetSmall": "snapshoturl in the [timelapse] section", "ConstantRateFactor": "Constant Rate Factor", "ConstantRateFactorDescription": "This configure quality vs file size of the rendered video. The range of the CRF scale is 0–51, where 0 is lossless, 23 is the default and 51 is worst quality possible. A lower value generally leads to higher quality and a subjectively sane range is 17–28. Consider 17 or 18 to be visually lossless.", "duplicatelastframe": "Duplicate Last Frame", @@ -1201,6 +1207,7 @@ "HyperlapseCycleDescription": "A snapshot will be taken any X seconds", "Mode": "Mode", "ModeDescription": "Select between Layer macro and Hyperlapse (time-based) mode", + "NoWebcamFound": "No Webcam available", "OutputFramerate": "Output Framerate", "OutputFramerateDescription": "Defines the framerate of the video. Note: this will be ignored if variable_fps is enabled", "Parkhead": "Park Toolhead", @@ -1231,6 +1238,7 @@ "RulesZeroAndPositive": "Value must be 0 or greater!", "SaveFrames": "Save Frames", "SaveFramesDescription": "Save the frames to a zip-file for external rendering", + "SelectWebcam": "Select webcam...", "StreamDelayCompensation": "Stream Delay Compensation", "StreamDelayCompensationDescription": "Delay frame capture", "Targetlength": "Target Length", @@ -1247,9 +1255,9 @@ "VariableFps": "Variable FPS", "VariableFpsDescription": "If enabled, the framerate of the output video will be calculated based on target length", "VariableFpsMax": "Variable FPS max", - "VariableFpsMaxDescription": "", + "VariableFpsMaxDescription": "Maximum variable FPS value", "VariableFpsMin": "Variable FPS min", - "VariableFpsMinDescription": "" + "VariableFpsMinDescription": "Minimum variable FPS value" }, "UiSettingsTab": { "BedScrewsDialog": "Bed Screws Dialog", @@ -1294,6 +1302,8 @@ "Logo": "Logo", "ManualProbeDialog": "Manual Probe Helper Dialog", "ManualProbeDialogDescription": "Display helper dialog for PROBE_CALIBRATE or Z_ENDSTOP_CALIBRATE.", + "Mode": "Mode", + "ModeDescription": "Change the overall look and feel of the application.", "NavigationStyle": "Navigation style", "NavigationStyleDescription": "Change navigation appearance", "NavigationStyleIconsAndText": "Icons + Text", @@ -1302,7 +1312,7 @@ "PowerDeviceNameDescription": "Select which Moonraker power device should be used to power on the printer.", "Primary": "Primary", "PrintstatusThumbnailZoom": "Large thumbnail zoom", - "PrintstatusThumbnailZoomDescription": "This will deactivate the zoom effect on the thumbnail in the status panel.", + "PrintstatusThumbnailZoomDescription": "This will deactivate the zoom effect of the thumbnail in the status panel.", "ProgressAsFavicon": "Show progress as favicon", "ProgressAsFaviconDescription": "Change the Mainsail logo favicon to a progress circle.", "ScrewsTiltAdjustDialog": "Screws Tilt Adjust Dialog", @@ -1311,7 +1321,7 @@ "TempchartHeightDescription": "Modify the height of the temperature chart on the Dashboard.", "Theme": "Theme", "ThemeDark": "Dark", - "ThemeDescription": "Change the overall look and feel of the application", + "ThemeDescription": "Customizes the branding of the interface.", "ThemeLight": "Light", "UiSettings": "UI-Settings" }, diff --git a/src/locales/uk.json b/src/locales/uk.json index 684818cc9..a42f3ceec 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -13,8 +13,11 @@ "DeprecatedOptionHeadline": "Застаріла опція Klipper", "DeprecatedValue": "Значення '{value}' опції '{option}' у секції '{section}' застаріла і буде видалена у майбутньому випуску.", "DeprecatedValueHeadline": "Застаріле значення Klipper", + "KlipperRuntimeWarning": "Попередження під час роботи Klipper", "KlipperWarning": "Попередження Klipper" }, + "MaintenanceReminder": "Нагадування про технічне обслуговування", + "MaintenanceReminderText": "Термін технічного обслуговування \"{name}\".", "MoonrakerWarnings": { "MoonrakerComponent": "Moonraker: {component}", "MoonrakerFailedComponentDescription": "Помилка була виявлена під час завантаження компонента Moonraker '{component}'. Будь ласка, перевірте файл журналу та виправте проблему.", @@ -28,7 +31,11 @@ "NextReboot": "наступне перезавантаження", "NoNotification": "Нема Повідомлень", "Notifications": "Повідомлення", - "Remind": "Нагадування:" + "OneDayShort": "1д", + "OneHourShort": "1год", + "OneWeekShort": "1тиж", + "Remind": "Нагадування:", + "ShowDetails": "показати деталі" }, "NumberInput": { "GreaterOrEqualError": "Повинен бути більшим або рівним, ніж {min}!", @@ -123,14 +130,14 @@ } }, "BedScrews": { - "Abort": "перервати", - "Accept": "прийняти", - "Adjusted": "відрегульований", - "Description": "Натисніть ADJUSTED, якщо поточний гвинт було відрегульовано. Натисніть ACCEPT, щоб продовжити без коригування.", + "Abort": "Перервати", + "Accept": "Прийняти", + "Adjusted": "Відкореговано", + "Description": "Натисніть ВІДКОРЕГОВАНО, якщо поточний гвинт було відрегульовано. Натисніть ПРИЙНЯТИ, щоб продовжити без коригування.", "Headline": "Гвинти столу", "ScrewAccepted": "Готові гвинти", - "ScrewIndex": "Індекс гвинта", - "ScrewName": "Ім'я гвинта", + "ScrewIndex": "Номер гвинта", + "ScrewName": "Назва гвинта", "ScrewOutput": "{current} з {max}" }, "ConnectionDialog": { @@ -139,7 +146,7 @@ "Connecting": "Підключення до {host}", "Failed": "Підключення не вдалося", "Initializing": "Ініціалізація", - "TryAgain": "спробуйте ще раз" + "TryAgain": "Спробуйте ще раз" }, "Console": { "CommandList": "Список команд", @@ -149,6 +156,12 @@ "SendCode": "Надіслати код...", "SetupConsole": "Консоль налаштування" }, + "CoolDownDialog": { + "AreYouSure": "Ти впевнений?", + "CoolDown": "Охолодження", + "No": "Ні", + "Yes": "Так" + }, "DevicesDialog": { "CanBusInfo": "Можна виявити лише непризначені вузли. Рекомендується мати лише один непризначений пристрій, підключений до шини can, щоб уникнути проблем зі зв’язком. Щоб отримати детальнішу інформацію, натисніть на посилання:", "ClickRefresh": "Натисніть кнопку оновити, щоб знайти пристрої.", @@ -160,7 +173,7 @@ "NoDeviceFound": "Пристрій не знайдено. Перевірте підключення та натисніть кнопку оновити.", "PathByHardware": "Фізичний шлях", "PathById": "Шлях за ID", - "Refresh": "оновити", + "Refresh": "Оновити", "Resolutions": "Резолюції" }, "Dialogs": { @@ -169,7 +182,7 @@ "DoYouWantToStartFilename": "Ви хочете роздрукувати {filename}?", "DoYouWantToStartFilenameFilament": "Хочете почати друк {filename} з наявного філаменту?", "Headline": "Почати друк", - "Print": "друк", + "Print": "Друк", "Timelapse": "Таймлапс" } }, @@ -316,7 +329,7 @@ "InvalidNameAlreadyExists": "Ім'я профілю вже існує, будь ласка, виберіть інше ім'я профілю.", "InvalidNameAscii": "Ім'я недійсне. Дозволяється лише символи ascii.", "InvalidNameEmpty": "Введення не повинно бути порожнім!", - "InvalidNameReserved": "Profile 'default' зарезервовано, будь ласка, виберіть інше ім'я профілю.", + "InvalidNameReserved": "Профіль 'default' зарезервовано, будь ласка, виберіть інше ім'я профілю.", "Mesh": "Сітка", "Name": "Ім'я", "NoBedMeshHasBeenLoadedYet": "Жодна сітка для ліжка ще не була завантажена.", @@ -334,6 +347,8 @@ "Wireframe": "Каркас" }, "History": { + "AddANote": "Додати примітку", + "AddMaintenance": "Додати обслуговування", "AddNote": "Додати коментар", "AddToQueueSuccessful": "Файл {filename} додано до черги.", "AllJobs": "ВСІ", @@ -341,16 +356,26 @@ "Cancel": "Скасувати", "Chart": "Графік", "CreateNote": "Створити Примітку", + "DateBasedReminder": "Дата", + "DateBasedReminderDescription": "Це нагадування базується на даті.", + "Days": "днів", "Delete": "Видалити", "DeleteSelectedQuestion": "Ви дійсно хочете видалити {count} вибране завдання?", "DeleteSingleJobQuestion": "Ви справді хочете видалити завдання?", "Details": "Деталі", + "EditMaintenance": "Редагувати технічне обслуговування", "EditNote": "Редагувати Примітку", "Empty": "порожньо", "EndTime": "Час Закінчення", + "EntryCreatedAt": "Створено {date}.", + "EntryNextPerform": "Наступне виконання:", + "EntryPerformedAt": "Виконано {date}.", + "EntrySince": "Використовується з:", "EstimatedFilament": "Орієнтовна довжина прутка", "EstimatedFilamentWeight": "Орієнтовна вага прутка", "EstimatedTime": "Орієнтовний Час", + "FilamentBasedReminder": "Філамент", + "FilamentBasedReminderDescription": "Це нагадування базується на використанні філаменту.", "FilamentCalc": "Калькулятор Прутка", "FilamentUsage": "Використання Прутка", "FilamentUsed": "Використано Прутка", @@ -361,18 +386,36 @@ "FirstLayerHeight": "Висота першого шару", "HistoryFilamentUsage": "Пруток", "HistoryPrinttimeAVG": "Друк", + "Hours": "годин", + "InvalidNameEmpty": "Недійсне ім'я. Поле не повинно бути пустим!", "JobDetails": "Деталі Завдань", "Jobs": "Завдання", "LastModified": "Дата Створення", "LayerHeight": "Висота Шару", "LoadCompleteHistory": "Завантажити повну історію", "LongestPrinttime": "Найдовший Час Друку", + "Maintenance": "Технічне обслуговування", + "MaintenanceEntries": "Записи технічного обслуговування", + "Meter": "метр", + "Name": "Назва", + "NoReminder": "Нагадування відсутні", "Note": "Примітка", "ObjectHeight": "Висота Об'єкта", + + "OneTime": "Одноразовий", + "Perform": "виконати", + "Performed": "виконано", + "PerformedAndReschedule": "виконано та переплановано", + "PerformMaintenance": "Виконати технічне обслуговування", "PrintDuration": "Час Друку", "PrintHistory": "Історія друку", + "PrintJobs": "Завдання друку", "PrintTime": "Час Друку", "PrinttimeAvg": "Час Друку - Ø", + "PrinttimeBasedReminder": "Час друку", + "PrinttimeBasedReminderDescription": "Це нагадування базується на часу друку.", + "Reminder": "Нагадування", + "Repeat": "Повторити", "Reprint": "Передрукувати", "Save": "зберегти", "Search": "пошук", @@ -589,7 +632,9 @@ "KlippyStatePanel": { "CheckKlippyAndUdsAddress": "Будь ласка, перевірте, чи працює служба Klipper і чи правильно налаштовано klippy_uds_address у файлі moonraker.conf.", "FirmwareRestart": "Перезапуск Прошивки", + "KlipperLog": "Журнал Klipper", "MoonrakerCannotConnect": "Moonraker не може підключитися до Klipper!", + "MoonrakerLog": "Журнал Moonraker", "PowerOn": "Увімкнено", "PrinterSwitchedOff": "Принтер вимкнений", "PrinterSwitchedOffDescription": "Принтер наразі вимкнено, і Klipper не може підключитися. Щоб увімкнути принтер, натисніть кнопку нижче:", @@ -608,7 +653,7 @@ }, "MacrosPanel": { "Headline": "Макрос", - "Send": "відправити" + "Send": "Відправити" }, "MiniconsolePanel": { "Autoscroll": "Автопрокручування", @@ -620,9 +665,9 @@ }, "MinSettingsPanel": { "IncludeMainsailCfg": "Переконайтеся, що ви включили mainsail.cfg у свій файл printer.cfg.", - "IsNotDefinedInConfig": "не визначається в конфігурації.", + "IsNotDefinedInConfig": "Відсутній імпорт в конфігурації.", "MissingConfiguration": "Відсутня конфігурація", - "MoreInformation": "більше інформації" + "MoreInformation": "Більше інформації" }, "MiscellaneousPanel": { "Headline": "Різне", @@ -801,11 +846,11 @@ "Webcam": "Веб-камера" }, "ScrewsTiltAdjust": { - "Accept": "прийняти", - "Base": "Основа", - "ErrorText": "Щось пішло не так під час процесу взяття проби.", - "Headline": "Гвинти регулювання нахилу", - "Retry": "повторити спробу" + "Accept": "Прийняти", + "Base": "Базовий", + "ErrorText": "Щось пішло не так під час взяття проби.", + "Headline": "Регулювання нахилу взяттям проби", + "Retry": "Повторити спробу" }, "SelectPrinterDialog": { "AddPrinter": "Додати Принтер", @@ -819,6 +864,8 @@ "HostnameInvalid": "Недійсне ім'я хоста/IP", "HostnameIp": "Ім'я хоста/IP", "HostnameRequired": "Необхідне ім'я хоста", + "Name": "Назва", + "Path": "Шлях", "Port": "Порт", "PortRequired": "Потрібен порт", "RememberToAdd": "Будь ласка, не забудьте додати '{cors}' у moonraker.conf всередині 'cors_domains'.", @@ -935,7 +982,8 @@ "DbConsoleHistory": "Історія Консолі", "DbHistoryJobs": "Історія завдань", "DbHistoryTotals": "Загальна Історія", - "DBNavigation": "Навігація", + "DbMaintenance": "Технічне обслуговування", + "DbNavigation": "Навігація", "DbTimelapseSettings": "Налаштування Таймлапсу", "DbView": "Налаштування перегляду", "EstimateValues": { @@ -1052,6 +1100,9 @@ "AddPrinter": "Додати принтер", "EditPrinter": "Редагувати принтер", "Hostname": "Ім'я хоста/ІР-адреса", + "Name": "Назва", + "NameDescription": "Це ім’я не відображатиметься в графічному інтерфейсі і використовуватиметься лише для перенаправлення.", + "Path": "Шлях", "Port": "Порт", "RemotePrinters": "Принтери", "UpdatePrinter": "Оновити принтер", @@ -1138,6 +1189,8 @@ "BoolBigThumbnailDescription": "Відобразити велику мініатюру на панелі статусу під час друку.", "BoolHideUploadAndPrintButton": "Приховати кнопку завантаження та друк", "BoolHideUploadAndPrintButtonDescription": "Показати або приховати кнопку «Завантажити та друканути» у верхній панелі.", + "ConfirmOnCoolDown": "Вимагати підтвердження для Охолодження", + "ConfirmOnCoolDownDescription": "Показати діалогове вікно підтвердження для Охолодження", "ConfirmOnEmergencyStop": "Вимагати підтвердження на АВАРІЙНУ ЗУПИНКУ", "ConfirmOnEmergencyStopDescription": "Показати діалогове вікно підтвердження АВАРІЙНОЇ ЗУПИНКИ", "ConfirmOnPowerDeviceChange": "Вимагати підтвердження змін живлення пристрою", @@ -1172,6 +1225,8 @@ "PowerDeviceName": "Пристрій живлення принтера", "PowerDeviceNameDescription": "Виберіть, який пристрій живлення Moonraker слід використовувати для живлення принтера.", "Primary": "Основний Колір", + "ProgressAsFavicon": "Показати прогрес у вкладці браузера", + "ProgressAsFaviconDescription": "Змініть іконку логотипу Mainsail на іконку прогресу.", "ScrewsTiltAdjustDialog": "Вікно налаштування гвинтів нахилу стола", "ScrewsTiltAdjustDialogDescription": "Відобразити допоміжне вікно для SCREWS_TILT_CALCULATE.", "TempchartHeight": "Висота графіку температур", diff --git a/src/locales/zh.json b/src/locales/zh.json index f33d907d1..9ff1fadbf 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -13,8 +13,11 @@ "DeprecatedOptionHeadline": "Klipper选项已被弃用", "DeprecatedValue": "在'{section}'标签中的'{option}'选项的参数'{value}'已被弃用,将在未来的版本中删除。", "DeprecatedValueHeadline": "Klipper参数已被弃用", + "KlipperRuntimeWarning": "Klipper运行时警告", "KlipperWarning": "Klipper警告" }, + "MaintenanceReminder": "保养提醒", + "MaintenanceReminderText": "\"{name}\" 已到保养期。", "MoonrakerWarnings": { "MoonrakerComponent": "Moonraker:{component}", "MoonrakerFailedComponentDescription": "加载moonraker组件'{component}'时发生错误。请检查日志文件并修复此问题。", @@ -28,7 +31,11 @@ "NextReboot": "下次重启", "NoNotification": "没有可用的通知", "Notifications": "通知", - "Remind": "提醒:" + "OneDayShort": "1天", + "OneHourShort": "1小时", + "OneWeekShort": "1周", + "Remind": "提醒:", + "ShowDetails": "显示详情" }, "NumberInput": { "GreaterOrEqualError": "必须大于或等于{min}!", @@ -149,6 +156,12 @@ "SendCode": "输入要执行的代码...", "SetupConsole": "设置控制台" }, + "CoolDownDialog": { + "AreYouSure": "是否确定?", + "CoolDown": "降温", + "No": "取消", + "Yes": "确定" + }, "DevicesDialog": { "CanBusInfo": "只有未分配的节点才能被检测到。建议仅连接一个未分配的设备到CAN总线,以避免通信问题。更多详情,请点击链接:", "ClickRefresh": "点击刷新按钮以搜索设备。", @@ -334,23 +347,35 @@ "Wireframe": "线框" }, "History": { - "AddNote": "添加便条", + "AddANote": "添加一条备注", + "AddMaintenance": "添加保养提醒", + "AddNote": "添加备注", "AddToQueueSuccessful": "文件{filename}已添加到队列。", "AllJobs": "全部", "AvgPrinttime": "平均打印时长", "Cancel": "取消", "Chart": "图表", - "CreateNote": "新建便条", + "CreateNote": "新建备注", + "DateBasedReminder": "日期", + "DateBasedReminderDescription": "本提醒基于日期。", + "Days": "天", "Delete": "删除", "DeleteSelectedQuestion": "你是否要删除已选中的{count}个任务?", "DeleteSingleJobQuestion": "你是否要删除这个任务?", "Details": "详情", - "EditNote": "编辑便条", + "EditMaintenance": "编辑保养提醒", + "EditNote": "编辑备注", "Empty": "没有内容", "EndTime": "结束时间", + "EntryCreatedAt": "创建于 {date}。", + "EntryNextPerform": "下次执行:", + "EntryPerformedAt": "已于 {date} 执行。", + "EntrySince": "启用时间:", "EstimatedFilament": "预估耗材用量", "EstimatedFilamentWeight": "预估耗材重量", "EstimatedTime": "预估打印时长", + "FilamentBasedReminder": "耗材", + "FilamentBasedReminderDescription": "本提醒基于耗材使用量。", "FilamentCalc": "耗材预估长度", "FilamentUsage": "耗材用量", "FilamentUsed": "实际耗材消耗量", @@ -361,18 +386,35 @@ "FirstLayerHeight": "首层高度", "HistoryFilamentUsage": "耗材用量", "HistoryPrinttimeAVG": "打印时长", + "Hours": "小时", + "InvalidNameEmpty": "无效名称。名称不能为空!", "JobDetails": "任务详情", "Jobs": "每页显示任务数", "LastModified": "修改日期", "LayerHeight": "层高", "LoadCompleteHistory": "加载完整历史记录", "LongestPrinttime": "最长打印时长", - "Note": "便条", + "Maintenance": "保养", + "MaintenanceEntries": "保养", + "Meter": "米", + "Name": "名称", + "NoReminder": "不提醒", + "Note": "备注", "ObjectHeight": "物体高度", + "OneTime": "一次性", + "Perform": "执行", + "Performed": "已执行", + "PerformedAndReschedule": "已执行并重新安排提醒", + "PerformMaintenance": "执行保养", "PrintDuration": "打印耗时", "PrintHistory": "打印历史", + "PrintJobs": "打印任务", "PrintTime": "打印持续时间", "PrinttimeAvg": "平均打印时长", + "PrinttimeBasedReminder": "打印时长", + "PrinttimeBasedReminderDescription": "本提醒基于打印时长", + "Reminder": "提醒", + "Repeat": "重复", "Reprint": "重新打印", "Save": "保存", "Search": "搜索", @@ -499,7 +541,7 @@ }, "UpdatePanel": { "Abort": "中止", - "AreYouSure": "你确定吗?", + "AreYouSure": "是否确定?", "CheckForUpdates": "检查更新", "Close": "关闭", "CommitHistory": "查看提交记录", @@ -589,7 +631,9 @@ "KlippyStatePanel": { "CheckKlippyAndUdsAddress": "请检查Klipper服务是否启动。", "FirmwareRestart": "重启Klipper固件", + "KlipperLog": "Klipper 日志", "MoonrakerCannotConnect": "Moonraker无法连接到Klipper !", + "MoonrakerLog": "Moonraker 日志", "PowerOn": "开启电源", "PrinterSwitchedOff": "打印机电源已关闭", "PrinterSwitchedOffDescription": "打印机电源已关闭,Klipper无法连接。点击下方按钮开启打印机电源:", @@ -738,8 +782,8 @@ "Presets": "预设", "SetupTemperatures": "配置温度", "ShowChart": "显示图表", - "ShowNameInChart": "在图表中显示{name} ", - "ShowNameInList": "在列表中显示{name} ", + "ShowNameInChart": "在图表中显示{name}", + "ShowNameInList": "在列表中显示{name}", "State": "状态", "Target": "目标", "TemperaturesInChart": "温度 [°C]", @@ -784,8 +828,8 @@ "PowerDeviceChangeDialog": { "AreYouSure": "是否确定?", "No": "取消", - "TurnDeviceOff": "关闭{device} ", - "TurnDeviceOn": "打开{device} ", + "TurnDeviceOff": "关闭{device}", + "TurnDeviceOn": "打开{device}", "Yes": "确定" }, "Router": { @@ -819,6 +863,8 @@ "HostnameInvalid": "不可用的主机名称/IP地址", "HostnameIp": "主机名称/IP地址", "HostnameRequired": "需要配置主机名称/IP地址", + "Name": "名称", + "Path": "路径", "Port": "Moonraker服务端口号", "PortRequired": "需要配置Moonraker服务端口号", "RememberToAdd": "请在moonraker.conf的'cors_domains'标签中添加'{cors}'", @@ -935,7 +981,8 @@ "DbConsoleHistory": "控制台历史记录", "DbHistoryJobs": "历史任务", "DbHistoryTotals": "历史总计", - "DBNavigation": "侧边栏", + "DbMaintenance": "保养", + "DbNavigation": "侧边栏", "DbTimelapseSettings": "延时摄影设置", "DbView": "视图设置", "EstimateValues": { @@ -1052,6 +1099,9 @@ "AddPrinter": "添加打印机", "EditPrinter": "编辑打印机", "Hostname": "主机名称/IP地址", + "Name": "名称", + "NameDescription": "此名称仅用于重定向,并不会在GUI上显示。", + "Path": "路径", "Port": "Moonraker服务端口", "RemotePrinters": "打印机", "UpdatePrinter": "更新打印机", @@ -1138,6 +1188,8 @@ "BoolBigThumbnailDescription": "打印时在状态框中显示大缩略图", "BoolHideUploadAndPrintButton": "隐藏\"上传并打印\"按钮", "BoolHideUploadAndPrintButtonDescription": "在顶栏显示或者隐藏\"上传并打印\"按钮", + "ConfirmOnCoolDown": "冷却需要确认", + "ConfirmOnCoolDownDescription": "在按下\"冷却\"时显示确认对话框", "ConfirmOnEmergencyStop": "紧急停止需要确认", "ConfirmOnEmergencyStopDescription": "在按下\"紧急停止\"时显示确认对话框", "ConfirmOnPowerDeviceChange": "控制设备电源时需要确认", @@ -1172,6 +1224,8 @@ "PowerDeviceName": "打印机电源设备", "PowerDeviceNameDescription": "请选择可以控制打印机供电的Moonraker电源设备。", "Primary": "高亮颜色", + "ProgressAsFavicon": "显示进度到浏览器标签", + "ProgressAsFaviconDescription": "将浏览器标签的Mainsail图标更改为进度圆圈。", "ScrewsTiltAdjustDialog": "螺丝倾斜调整对话框", "ScrewsTiltAdjustDialogDescription": "显示SCREWS_TILT_CALCULATE辅助对话框。", "TempchartHeight": "温度图表高度", diff --git a/src/main.ts b/src/main.ts index 269588c33..7cff4da1c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,7 +35,7 @@ import { DatasetComponent, GridComponent, LegendComponent, TooltipComponent } fr import 'vue-resize/dist/vue-resize.css' // @ts-ignore import VueResize from 'vue-resize' -import { defaultTheme } from './store/variables' +import { defaultMode } from './store/variables' Vue.config.productionTip = false @@ -66,8 +66,11 @@ Vue.use(VueResize) const initLoad = async () => { try { + // get base url. by default, it is '/' + const base = import.meta.env.BASE_URL ?? '/' + //load config.json - const res = await fetch('/config.json') + const res = await fetch(`${base}config.json`) const file = (await res.json()) as Record window.console.debug('Loaded config.json') @@ -77,9 +80,9 @@ const initLoad = async () => { await setAndLoadLocale(file.defaultLocale as string) } - // Handle theme outside of store init and before vue mount for consistency in dialog - const theme = file.defaultTheme ?? defaultTheme - vuetify.framework.theme.dark = theme !== 'light' + // Handle mode outside store init and before vue mount for consistency in dialog + const mode = file.defaultMode ?? defaultMode + vuetify.framework.theme.dark = mode !== 'light' } catch (e) { window.console.error('Failed to load config.json') window.console.error(e) diff --git a/src/plugins/router.ts b/src/plugins/router.ts index f211d1918..2d31ce432 100644 --- a/src/plugins/router.ts +++ b/src/plugins/router.ts @@ -3,6 +3,10 @@ import Vue from 'vue' import routes from '@/routes' Vue.use(VueRouter) -const router = new VueRouter({ mode: 'history', routes }) +const router = new VueRouter({ + base: import.meta.env.BASE_URL, + mode: 'history', + routes, +}) export default router diff --git a/src/store/gui/getters.ts b/src/store/gui/getters.ts index 265dba619..58ac58043 100644 --- a/src/store/gui/getters.ts +++ b/src/store/gui/getters.ts @@ -1,10 +1,24 @@ import { GetterTree } from 'vuex' import { GuiState } from '@/store/gui/types' import { GuiMacrosStateMacrogroup } from '@/store/gui/macros/types' -import { allDashboardPanels } from '@/store/variables' +import { allDashboardPanels, defaultTheme, themes } from '@/store/variables' +import { Theme } from '@/store/types' // eslint-disable-next-line export const getters: GetterTree = { + theme: (state): string => { + const theme = state.uiSettings.theme + + // return defaultTheme, if theme doesnt exists + if (themes.findIndex((tmp: Theme) => tmp.name === theme) === -1) return defaultTheme + + return theme + }, + + getTheme: (state, getters): Theme => { + return themes.find((theme: Theme) => theme.name === getters.theme) ?? themes[0] + }, + getDatasetValue: (state) => (payload: { name: string; type: string }) => { if ( payload.name in state.view.tempchart.datasetSettings && diff --git a/src/store/gui/index.ts b/src/store/gui/index.ts index 81a3ecc0a..579e686a7 100644 --- a/src/store/gui/index.ts +++ b/src/store/gui/index.ts @@ -3,7 +3,13 @@ import { Module } from 'vuex' import { actions } from '@/store/gui/actions' import { mutations } from '@/store/gui/mutations' import { getters } from '@/store/gui/getters' -import { defaultTheme, defaultLogoColor, defaultPrimaryColor, defaultBigThumbnailBackground } from '@/store/variables' +import { + defaultTheme, + defaultLogoColor, + defaultPrimaryColor, + defaultBigThumbnailBackground, + defaultMode, +} from '@/store/variables' // load modules import { console } from '@/store/gui/console' @@ -155,6 +161,7 @@ export const getDefaultState = (): GuiState => { entries: [], }, uiSettings: { + mode: defaultMode, theme: defaultTheme, logo: defaultLogoColor, primary: defaultPrimaryColor, diff --git a/src/store/gui/maintenance/actions.ts b/src/store/gui/maintenance/actions.ts index 3205bcba2..1f3530dff 100644 --- a/src/store/gui/maintenance/actions.ts +++ b/src/store/gui/maintenance/actions.ts @@ -36,6 +36,14 @@ export const actions: ActionTree = { // stop, when no entries are available/found const entries = defaults.entries ?? [] if (entries?.length === 0) { + Vue.$socket.emit('server.database.post_item', { + namespace: 'maintenance', + key: uuidv4(), + value: { + name: 'MAINTENANCE_INIT', + }, + }) + return } @@ -92,7 +100,12 @@ export const actions: ActionTree = { async initStore({ commit, dispatch }, payload) { await commit('reset') - await commit('initStore', payload) + + const entries = payload.value ?? {} + const initKey = Object.keys(entries).find((key) => entries[key]?.name === 'MAINTENANCE_INIT') + if (initKey) delete entries[initKey] + + await commit('initStore', entries) await dispatch('socket/removeInitModule', 'gui/maintenance/init', { root: true }) }, diff --git a/src/store/gui/maintenance/getters.ts b/src/store/gui/maintenance/getters.ts index 2801a5e67..497316987 100644 --- a/src/store/gui/maintenance/getters.ts +++ b/src/store/gui/maintenance/getters.ts @@ -24,7 +24,7 @@ export const getters: GetterTree = { if (entry.reminder.type === null || entry.end_time !== null) return false if (entry.reminder.filament.bool) { - const end = entry.start_filament + (entry.reminder.filament.value ?? 0) + const end = entry.start_filament + (entry.reminder.filament.value ?? 0) * 1000 if (end <= currentTotalFilamentUsed) return true } diff --git a/src/store/gui/maintenance/mutations.ts b/src/store/gui/maintenance/mutations.ts index 7f9dee174..e7d559d9a 100644 --- a/src/store/gui/maintenance/mutations.ts +++ b/src/store/gui/maintenance/mutations.ts @@ -9,7 +9,7 @@ export const mutations: MutationTree = { }, initStore(state, payload) { - Vue.set(state, 'entries', payload.value) + Vue.set(state, 'entries', payload) }, store(state, payload) { diff --git a/src/store/gui/notifications/getters.ts b/src/store/gui/notifications/getters.ts index 11c77390a..4380da12e 100644 --- a/src/store/gui/notifications/getters.ts +++ b/src/store/gui/notifications/getters.ts @@ -42,6 +42,9 @@ export const getters: GetterTree = { // browser warnings notifications = notifications.concat(getters['getNotificationsBrowserWarnings']) + // TMC overheat warnings + notifications = notifications.concat(getters['getNotificationsOverheatDrivers']) + const mapType = { normal: 2, high: 1, @@ -399,6 +402,44 @@ export const getters: GetterTree = { return notifications }, + getNotificationsOverheatDrivers: (state, getters, rootState) => { + const notifications: GuiNotificationStateEntry[] = [] + const date = rootState.server.system_boot_at ?? new Date() + + Object.keys(rootState.printer) + .filter((key) => key.startsWith('tmc')) + .forEach((key) => { + const printerObject = rootState.printer[key] + const name = key.split(' ')[1] + + if ((printerObject.drv_status?.ot ?? null) === 1) { + notifications.push({ + id: `tmcwarning/${key}-ot`, + priority: 'critical', + title: i18n.t('App.Notifications.TmcOtFlag').toString(), + description: i18n.t('App.Notifications.TmcOtFlagText', { name }).toString(), + date, + dismissed: false, + url: 'https://www.klipper3d.org/TMC_Drivers.html#tmc-reports-error-ot1overtemperror', + }) + } + + if ((printerObject.drv_status?.otpw ?? null) === 1) { + notifications.push({ + id: `tmcwarning/${key}-otpw`, + priority: 'high', + title: i18n.t('App.Notifications.TmcOtpwFlag').toString(), + description: i18n.t('App.Notifications.TmcOtpwFlagText', { name }).toString(), + date, + dismissed: false, + url: 'https://www.klipper3d.org/TMC_Drivers.html#tmc-reports-error-ot1overtemperror', + }) + } + }) + + return notifications + }, + getDismiss: (state, getters, rootState) => { const currentTime = new Date() const systemBootAt = rootState.server.system_boot_at ?? new Date() diff --git a/src/store/gui/types.ts b/src/store/gui/types.ts index aeb25d43d..c6d654f49 100644 --- a/src/store/gui/types.ts +++ b/src/store/gui/types.ts @@ -103,7 +103,8 @@ export interface GuiState { presets?: GuiPresetsState remoteprinters?: GuiRemoteprintersState uiSettings: { - theme: 'dark' | 'light' + mode: 'dark' | 'light' + theme: string logo: string primary: string displayCancelPrint: boolean diff --git a/src/store/printer/getters.ts b/src/store/printer/getters.ts index 1e031dd64..51ba0beb2 100644 --- a/src/store/printer/getters.ts +++ b/src/store/printer/getters.ts @@ -146,24 +146,33 @@ export const getters: GetterTree = { getMacros: (state) => { const array: PrinterStateMacro[] = [] - const config = state.configfile?.config ?? {} const settings = state.configfile?.settings ?? null + const printerGcodes = state.gcode?.commands ?? {} - Object.keys(config) - .filter((prop) => prop.startsWith('gcode_macro') && !settings[prop.toLowerCase()]?.rename_existing) + const prefix = 'gcode_macro ' + const prefixLength = prefix.length + + Object.keys(state) + .filter((prop) => prop.toLowerCase().startsWith(prefix)) .forEach((prop) => { - const name = prop.replace('gcode_macro ', '') + const name = prop.slice(prefixLength) + const printerGcode = printerGcodes[name.toUpperCase()] ?? {} const variables = state[prop] ?? {} const hints = variables?.front_end_hints + + // remove macros with a '_' as first char + if (hints && 'hidden' in hints ? hints.hidden : name.startsWith('_')) return + // remove macros with rename_existing in the config const propLower = prop.toLowerCase() - const propSettings = settings[propLower] + const propSettings = settings[propLower] ?? {} + if ('rename_existing' in propSettings) return array.push({ name, alias: '', - description: settings[propLower].description ?? null, + description: printerGcode?.help ?? null, prop: propSettings, params: hints ? getMacroParamsFromState(hints) : getMacroParamsFromConfig(propSettings), hidden: hints && 'hidden' in hints ? hints.hidden : name.startsWith('_'), @@ -480,7 +489,9 @@ export const getters: GetterTree = { Object.keys(state).forEach((key) => { if (key === 'mcu' || key.startsWith('mcu ')) { const mcu = state[key] - const versionOutput = (mcu.mcu_version ?? 'unknown').split('-').slice(0, 4).join('-') + let versionOutput = (mcu.mcu_version ?? 'unknown').split('-').slice(0, 4).join('-') + + if ('app' in mcu && mcu.app !== 'Klipper') versionOutput = mcu.app + ' ' + versionOutput let load = 0 if (mcu.last_stats?.mcu_task_avg && mcu.last_stats?.mcu_task_stddev) { diff --git a/src/store/printer/tempHistory/getters.ts b/src/store/printer/tempHistory/getters.ts index ac182d6a6..ab74bbbe5 100644 --- a/src/store/printer/tempHistory/getters.ts +++ b/src/store/printer/tempHistory/getters.ts @@ -104,7 +104,7 @@ export const getters: GetterTree = { Object.keys(viewSettings).forEach((key) => { // break if this element doesn't exist in available_sensors - if (!(available_sensors.includes(key) || available_monitors.includes(key))) return + if (!available_sensors.includes(key) && !available_monitors.includes(key)) return Object.keys(viewSettings[key]).forEach((attrKey) => { // break if this element isn't a valid datasetType diff --git a/src/store/server/actions.ts b/src/store/server/actions.ts index 8fb762b75..75c11cc30 100644 --- a/src/store/server/actions.ts +++ b/src/store/server/actions.ts @@ -59,15 +59,15 @@ export const actions: ActionTree = { dispatch('socket/addInitModule', 'gui/init', { root: true }) dispatch('gui/init', null, { root: true }) } else dispatch('gui/initDb', null, { root: true }) - if (payload.namespaces?.includes('webcams')) { - dispatch('socket/addInitModule', 'gui/webcam/init', { root: true }) - dispatch('gui/webcams/init', null, { root: true }) - } if (payload.namespaces?.includes('maintenance')) { dispatch('socket/addInitModule', 'gui/maintenance/init', { root: true }) dispatch('gui/maintenance/init', null, { root: true }) } else dispatch('gui/maintenance/initDb', null, { root: true }) + // init webcams + dispatch('socket/addInitModule', 'gui/webcam/init', { root: true }) + dispatch('gui/webcams/init', null, { root: true }) + commit('saveDbNamespaces', payload.namespaces) Vue.$socket.emit('server.info', {}, { action: 'server/checkKlippyConnected' }) diff --git a/src/store/server/history/actions.ts b/src/store/server/history/actions.ts index 455fc8c4c..3a2eda0cf 100644 --- a/src/store/server/history/actions.ts +++ b/src/store/server/history/actions.ts @@ -19,6 +19,11 @@ export const actions: ActionTree = { getTotals({ commit }, payload) { commit('setTotals', payload.job_totals) + + const auxiliary_totals = payload.auxiliary_totals ?? [] + if (auxiliary_totals.length) { + commit('setAuxiliaryTotals', auxiliary_totals) + } }, async getHistory({ commit, dispatch, state }, payload) { diff --git a/src/store/server/history/index.ts b/src/store/server/history/index.ts index 8e1acead3..c16abbfd5 100644 --- a/src/store/server/history/index.ts +++ b/src/store/server/history/index.ts @@ -15,6 +15,7 @@ export const getDefaultState = (): ServerHistoryState => { longest_job: 0, longest_print: 0, }, + auxiliary_totals: [], all_loaded: false, } } diff --git a/src/store/server/history/mutations.ts b/src/store/server/history/mutations.ts index 9ee7a3129..d4df8fded 100644 --- a/src/store/server/history/mutations.ts +++ b/src/store/server/history/mutations.ts @@ -16,6 +16,10 @@ export const mutations: MutationTree = { Vue.set(state, 'job_totals', payload) }, + setAuxiliaryTotals(state, payload) { + Vue.set(state, 'auxiliary_totals', payload) + }, + setHistoryNotes(state, payload) { const job = state.jobs.find((job) => job.job_id === payload.job_id) if (job) Vue.set(job, 'note', payload.text) diff --git a/src/store/server/history/types.ts b/src/store/server/history/types.ts index 4d8cd56fc..d9e69e5fa 100644 --- a/src/store/server/history/types.ts +++ b/src/store/server/history/types.ts @@ -10,6 +10,7 @@ export interface ServerHistoryState { longest_job: number longest_print: number } + auxiliary_totals: ServerHistoryStateJobAuxiliaryTotal[] all_loaded: boolean } @@ -50,6 +51,22 @@ export interface ServerHistoryStateJob { status: string start_time: number total_duration: number + auxiliary_data?: ServerHistoryStateJobAuxiliaryData[] +} + +export interface ServerHistoryStateJobAuxiliaryData { + description: string + name: string + provider: string + units: string + value: number | number[] +} + +export interface ServerHistoryStateJobAuxiliaryTotal { + field: string + maximum: number + provider: string + total: number } export interface HistoryListRowJob extends ServerHistoryStateJob { diff --git a/src/store/server/index.ts b/src/store/server/index.ts index 1fde111a3..5e90dc7dc 100644 --- a/src/store/server/index.ts +++ b/src/store/server/index.ts @@ -12,6 +12,7 @@ import { timelapse } from '@/store/server/timelapse' import { jobQueue } from '@/store/server/jobQueue' import { announcements } from '@/store/server/announcements' import { spoolman } from '@/store/server/spoolman' +import { sensor } from '@/store/server/sensor' // create getDefaultState export const getDefaultState = (): ServerState => { @@ -62,5 +63,6 @@ export const server: Module = { jobQueue, announcements, spoolman, + sensor, }, } diff --git a/src/store/server/sensor/actions.ts b/src/store/server/sensor/actions.ts new file mode 100644 index 000000000..28addc301 --- /dev/null +++ b/src/store/server/sensor/actions.ts @@ -0,0 +1,26 @@ +import Vue from 'vue' +import { ActionTree } from 'vuex' +import { ServerSensorState } from '@/store/server/sensor/types' +import { RootState } from '@/store/types' + +export const actions: ActionTree = { + reset({ commit }) { + commit('reset') + }, + + init() { + Vue.$socket.emit('server.sensors.list', {}, { action: 'server/sensor/getSensors' }) + }, + + getSensors({ commit, dispatch }, payload) { + commit('setSensors', payload.sensors) + + dispatch('socket/removeInitModule', 'server/sensor/init', { root: true }) + }, + + updateSensors({ commit }, payload) { + Object.keys(payload).forEach((key) => { + commit('updateSensor', { key, value: payload[key] }) + }) + }, +} diff --git a/src/store/server/sensor/getters.ts b/src/store/server/sensor/getters.ts new file mode 100644 index 000000000..10028f160 --- /dev/null +++ b/src/store/server/sensor/getters.ts @@ -0,0 +1,9 @@ +import { GetterTree } from 'vuex' +import { ServerSensorState } from '@/store/server/sensor/types' + +// eslint-disable-next-line +export const getters: GetterTree = { + getSensors: (state) => { + return Object.keys(state.sensors) + }, +} diff --git a/src/store/server/sensor/index.ts b/src/store/server/sensor/index.ts new file mode 100644 index 000000000..c60aea2c9 --- /dev/null +++ b/src/store/server/sensor/index.ts @@ -0,0 +1,23 @@ +import { Module } from 'vuex' +import { ServerSensorState } from '@/store/server/sensor/types' +import { actions } from '@/store/server/sensor/actions' +import { mutations } from '@/store/server/sensor/mutations' +import { getters } from '@/store/server/sensor/getters' + +export const getDefaultState = (): ServerSensorState => { + return { + sensors: {}, + } +} + +// initial state +const state = getDefaultState() + +// eslint-disable-next-line +export const sensor: Module = { + namespaced: true, + state, + getters, + actions, + mutations, +} diff --git a/src/store/server/sensor/mutations.ts b/src/store/server/sensor/mutations.ts new file mode 100644 index 000000000..2e9a7b841 --- /dev/null +++ b/src/store/server/sensor/mutations.ts @@ -0,0 +1,20 @@ +import Vue from 'vue' +import { getDefaultState } from './index' +import { MutationTree } from 'vuex' +import { ServerSensorState } from '@/store/server/sensor/types' + +export const mutations: MutationTree = { + reset(state) { + Object.assign(state, getDefaultState()) + }, + + setSensors(state, payload) { + Vue.set(state, 'sensors', payload) + }, + + updateSensor(state, payload) { + if (!(payload.key in state.sensors)) return + + Vue.set(state.sensors[payload.key], 'values', payload.value) + }, +} diff --git a/src/store/server/sensor/types.ts b/src/store/server/sensor/types.ts new file mode 100644 index 000000000..e745b04d3 --- /dev/null +++ b/src/store/server/sensor/types.ts @@ -0,0 +1,14 @@ +export interface ServerSensorState { + sensors: { + [key: string]: ServerSensorStateSensor + } +} + +export interface ServerSensorStateSensor { + friendly_name: string + id: string + type: string + values: { + [key: string]: number + } +} diff --git a/src/store/socket/actions.ts b/src/store/socket/actions.ts index 50b66b16b..0a207d48b 100644 --- a/src/store/socket/actions.ts +++ b/src/store/socket/actions.ts @@ -135,6 +135,10 @@ export const actions: ActionTree = { dispatch('server/spoolman/getActiveSpoolId', payload.params[0], { root: true }) break + case 'notify_sensor_update': + dispatch('server/sensor/updateSensors', payload.params[0], { root: true }) + break + default: window.console.debug(payload) } diff --git a/src/store/types.ts b/src/store/types.ts index 8efa504a2..f0aeea56b 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -38,3 +38,22 @@ export interface ConfigJsonInstance { port?: number path?: string } + +export interface Theme { + name: string + displayName: string + colorLogo: string + colorPrimary?: string + logo?: { + show: boolean + light: boolean + } + sidebarBackground?: { + show: boolean + light: boolean + } + mainBackground?: { + show: boolean + light: boolean + } +} diff --git a/src/store/variables.ts b/src/store/variables.ts index f3676d781..23a7a4033 100644 --- a/src/store/variables.ts +++ b/src/store/variables.ts @@ -1,4 +1,7 @@ -export const defaultTheme = 'dark' +import { Theme } from '@/store/types' + +export const defaultMode = 'dark' +export const defaultTheme = 'mainsail' export const defaultLogoColor = '#D41216' export const defaultPrimaryColor = '#2196f3' export const defaultBigThumbnailBackground = '#1e1e1e' @@ -35,6 +38,7 @@ export const initableServerComponents = [ 'jobQueue', 'announcements', 'spoolman', + 'sensor', ] /* @@ -139,3 +143,63 @@ export const genericLogfiles = ['klippy', 'moonraker', 'crowsnest', 'mmu', 'sona * List of all rollover logfiles */ export const rolloverLogfiles = ['klipper', 'moonraker'] + +/* + * List of all Themes + */ +export const themes: Theme[] = [ + { name: 'mainsail', displayName: 'Mainsail', colorLogo: defaultLogoColor }, + { + name: 'klipper', + displayName: 'Klipper', + colorLogo: '#b12f35', + logo: { show: true, light: false }, + }, + { + name: 'voron', + displayName: 'Voron Design', + colorLogo: '#FF2300', + logo: { show: true, light: false }, + }, + { + name: 'ldo', + displayName: 'LDO Motion (Sponsor)', + colorLogo: '#326799', + colorPrimary: '#326799', + logo: { show: true, light: false }, + }, + { + name: 'yumi', + displayName: 'YUMI (Sponsor)', + colorLogo: '#F6CF3D', + colorPrimary: '#F6CF3D', + logo: { show: true, light: false }, + }, + { + name: 'vzbot', + displayName: 'VzBot', + colorLogo: '#FF0000', + logo: { show: true, light: false }, + sidebarBackground: { show: true, light: false }, + }, + { + name: 'prusa', + displayName: 'Prusa Research (Sponsor)', + colorLogo: '#fa6831', + colorPrimary: '#fa6831', + logo: { show: true, light: false }, + }, + { + name: 'btt', + displayName: 'BigTreeTech (Sponsor)', + colorLogo: '#ef0025', + logo: { show: true, light: false }, + }, + { + name: 'multec', + displayName: 'Multec GmbH (Sponsor)', + colorLogo: '#234D7A', + colorPrimary: '#234D7A', + logo: { show: true, light: false }, + }, +]