Improve web: dim disabled, icons, pin

This commit is contained in:
Joseph Abbey
2025-07-18 22:18:26 +01:00
parent a6b4925ff7
commit 460f247728
6 changed files with 99 additions and 27 deletions

View File

@@ -2,5 +2,8 @@
"cSpell.words": [ "cSpell.words": [
"usbs", "usbs",
"Venu" "Venu"
] ],
"files.exclude": {
"resources-*": true
}
} }

View File

@@ -37,7 +37,8 @@
margin-left: 0.5em; margin-left: 0.5em;
filter: grayscale() invert(); filter: grayscale() invert();
} }
.template, .info { .template,
.info {
background-image: url(../resources-icons-48/info_type.svg); background-image: url(../resources-icons-48/info_type.svg);
background-size: contain; background-size: contain;
margin-left: 0.5em; margin-left: 0.5em;
@@ -63,6 +64,13 @@
margin-left: 0.5em; margin-left: 0.5em;
filter: grayscale() invert(); filter: grayscale() invert();
} }
.mdi {
margin-left: 0.5em;
}
.disabled {
opacity: 0.5;
}
:root { :root {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
@@ -285,6 +293,10 @@
background-color: var(--ctp-mocha-overlay1); background-color: var(--ctp-mocha-overlay1);
} }
</style> </style>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css"
defer />
</head> </head>
<body> <body>
<div id="settings"> <div id="settings">
@@ -441,9 +453,10 @@ http:
</div> </div>
</dialog> </dialog>
<script src="https://www.unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script> <script src="https://www.unpkg.com/monaco-editor@0.52.2/min/vs/loader.js"></script>
<script src="https://www.unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script> <script src="https://www.unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script>
<script src="https://www.unpkg.com/toastify-js@1.12.0/src/toastify.js"></script> <script src="https://www.unpkg.com/toastify-js@1.12.0/src/toastify.js"></script>
<script src="https://code.iconify.design/1/1.0.6/iconify.min.js"></script>
<script type="module" src="./main.js"></script> <script type="module" src="./main.js"></script>
</body> </body>
</html> </html>

View File

@@ -11,7 +11,7 @@ let api_token = localStorage.getItem('api_token') ?? '';
/** /**
* Get all entities in HomeAssistant. * Get all entities in HomeAssistant.
* @returns {Promise<Record<string, string>>} [id, name] * @returns {Promise<Record<string, { name: string, icon?: string }>>} [id, name]
*/ */
async function get_entities() { async function get_entities() {
try { try {
@@ -21,7 +21,7 @@ async function get_entities() {
Authorization: `Bearer ${api_token}`, Authorization: `Bearer ${api_token}`,
}, },
mode: 'cors', mode: 'cors',
body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`, body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\",\\"{{ entity.attributes.icon }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
}); });
if (res.status == 401 || res.status == 403) { if (res.status == 401 || res.status == 403) {
document.querySelector('#api_token').classList.add('invalid'); document.querySelector('#api_token').classList.add('invalid');
@@ -29,8 +29,16 @@ async function get_entities() {
} }
document.querySelector('#api_url').classList.remove('invalid'); document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid'); document.querySelector('#api_token').classList.remove('invalid');
return Object.fromEntries(await res.json()); const data = {};
} catch { for (const [id, name, icon] of await res.json()) {
data[id] = { name };
if (icon !== '') {
data[id].icon = icon;
}
}
return data;
} catch (e) {
console.error('Error fetching entities:', e);
document.querySelector('#api_url').classList.add('invalid'); document.querySelector('#api_url').classList.add('invalid');
return {}; return {};
} }
@@ -57,7 +65,8 @@ async function get_devices() {
document.querySelector('#api_url').classList.remove('invalid'); document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid'); document.querySelector('#api_token').classList.remove('invalid');
return Object.fromEntries(await res.json()); return Object.fromEntries(await res.json());
} catch { } catch (e) {
console.error('Error fetching devices:', e);
document.querySelector('#api_url').classList.add('invalid'); document.querySelector('#api_url').classList.add('invalid');
return {}; return {};
} }
@@ -84,7 +93,8 @@ async function get_areas() {
document.querySelector('#api_url').classList.remove('invalid'); document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid'); document.querySelector('#api_token').classList.remove('invalid');
return Object.fromEntries(await res.json()); return Object.fromEntries(await res.json());
} catch { } catch (e) {
console.error('Error fetching areas:', e);
document.querySelector('#api_url').classList.add('invalid'); document.querySelector('#api_url').classList.add('invalid');
return {}; return {};
} }
@@ -119,7 +129,8 @@ async function get_services() {
} }
} }
return services; return services;
} catch { } catch (e) {
console.error('Error fetching services:', e);
document.querySelector('#api_url').classList.add('invalid'); document.querySelector('#api_url').classList.add('invalid');
return []; return [];
} }
@@ -370,6 +381,9 @@ async function generate_schema(entities, devices, areas, services, schema) {
confirm: { confirm: {
$ref: '#/$defs/confirm', $ref: '#/$defs/confirm',
}, },
pin: {
$ref: '#/$defs/pin',
},
data: { data: {
type: 'object', type: 'object',
properties: {}, properties: {},
@@ -724,6 +738,7 @@ require(['vs/editor/editor.main'], async () => {
var decorations = editor.createDecorationsCollection([]); var decorations = editor.createDecorationsCollection([]);
/** @type {monaco.editor.IMarkerData[]} */
let markers = []; let markers = [];
const renderTemplate = editor.addCommand( const renderTemplate = editor.addCommand(
@@ -905,6 +920,7 @@ require(['vs/editor/editor.main'], async () => {
const ast = json.parse(model.getValue()); const ast = json.parse(model.getValue());
const data = JSON.parse(model.getValue()); const data = JSON.parse(model.getValue());
markers = []; markers = [];
/** @type {monaco.editor.IModelDeltaDecoration[]} */
const glyphs = []; const glyphs = [];
async function testToggle(range, entity) { async function testToggle(range, entity) {
const res = await fetch(api_url + '/states/' + entity, { const res = await fetch(api_url + '/states/' + entity, {
@@ -983,8 +999,10 @@ require(['vs/editor/editor.main'], async () => {
* @param {import('json-ast-comments').JsonAst | * @param {import('json-ast-comments').JsonAst |
* import('json-ast-comments').JsonProperty} node * import('json-ast-comments').JsonProperty} node
* @param {string[]} path * @param {string[]} path
* @param {import('json-ast-comments').JsonAst |
* import('json-ast-comments').JsonProperty | null} parent
*/ */
function recurse(node, path) { function recurse(node, path, parent = null) {
if (node.type === 'property') { if (node.type === 'property') {
if (node.key[0].value === 'content') { if (node.key[0].value === 'content') {
templates.push([ templates.push([
@@ -1010,11 +1028,39 @@ require(['vs/editor/editor.main'], async () => {
} }
trim++; trim++;
markers.push({ markers.push({
message: entities[node.value[0].value] ?? 'Entity not found', message: entities[node.value[0].value].name ?? 'Entity not found',
severity: monaco.MarkerSeverity.Hint, severity: monaco.MarkerSeverity.Hint,
...range, ...range,
startColumn: trim, startColumn: trim,
}); });
glyphs.push({
range,
options: {
isWholeLine: true,
glyphMarginClassName:
'mdi ' +
entities[node.value[0].value]?.icon?.replace(':', '-'),
},
});
} else if (
node.key[0].value === 'enabled' &&
node.value[0].type === 'boolean' &&
!node.value[0].value
) {
glyphs.push({
range: {
startLineNumber: parent.members[0].key[0].range.start.line + 1,
startColumn: 0,
endLineNumber:
parent.members[parent.members.length - 1].value[0].range.end
.line + 1,
endColumn: 10000,
},
options: {
isWholeLine: true,
inlineClassName: 'disabled',
},
});
} else if (node.key[0].value === 'type') { } else if (node.key[0].value === 'type') {
if (node.value[0].value === 'toggle') { if (node.value[0].value === 'toggle') {
toggles.push([ toggles.push([
@@ -1041,15 +1087,15 @@ require(['vs/editor/editor.main'], async () => {
}); });
} }
} else { } else {
recurse(node.value[0], [...path, node.key[0].value]); recurse(node.value[0], [...path, node.key[0].value], node);
} }
} else if (node.type === 'array') { } else if (node.type === 'array') {
for (let i = 0; i < node.members.length; i++) { for (let i = 0; i < node.members.length; i++) {
recurse(node.members[i], [...path, i]); recurse(node.members[i], [...path, i], node);
} }
} else if (node.type === 'object') { } else if (node.type === 'object') {
for (let member of node.members) { for (let member of node.members) {
recurse(member, path); recurse(member, path, node);
} }
} }
} }

View File

@@ -10,10 +10,11 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/toastify-js": "^1.12.3", "@types/toastify-js": "^1.12.4",
"@vscode/webview-ui-toolkit": "1.4.0", "@vscode/webview-ui-toolkit": "1.4.0",
"json-ast-comments": "1.1.1", "json-ast-comments": "1.1.1",
"monaco-editor": "0.45.0", "monaco-editor": "0.52.2",
"prettier": "^3.6.2",
"serve": "^14.2.1" "serve": "^14.2.1"
} }
} }

25
web/pnpm-lock.yaml generated
View File

@@ -6,8 +6,8 @@ settings:
devDependencies: devDependencies:
'@types/toastify-js': '@types/toastify-js':
specifier: ^1.12.3 specifier: ^1.12.4
version: 1.12.3 version: 1.12.4
'@vscode/webview-ui-toolkit': '@vscode/webview-ui-toolkit':
specifier: 1.4.0 specifier: 1.4.0
version: 1.4.0(react@18.2.0) version: 1.4.0(react@18.2.0)
@@ -15,8 +15,11 @@ devDependencies:
specifier: 1.1.1 specifier: 1.1.1
version: 1.1.1 version: 1.1.1
monaco-editor: monaco-editor:
specifier: 0.45.0 specifier: 0.52.2
version: 0.45.0 version: 0.52.2
prettier:
specifier: ^3.6.2
version: 3.6.2
serve: serve:
specifier: ^14.2.1 specifier: ^14.2.1
version: 14.2.1 version: 14.2.1
@@ -52,8 +55,8 @@ packages:
exenv-es6: 1.1.1 exenv-es6: 1.1.1
dev: true dev: true
/@types/toastify-js@1.12.3: /@types/toastify-js@1.12.4:
resolution: {integrity: sha512-9RjLlbAHMSaae/KZNHGv19VG4gcLIm3YjvacCXBtfMfYn26h76YP5oxXI8k26q4iKXCB9LNfv18lsoS0JnFPTg==} resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==}
dev: true dev: true
/@vscode/webview-ui-toolkit@1.4.0(react@18.2.0): /@vscode/webview-ui-toolkit@1.4.0(react@18.2.0):
@@ -415,8 +418,8 @@ packages:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: true dev: true
/monaco-editor@0.45.0: /monaco-editor@0.52.2:
resolution: {integrity: sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==} resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
dev: true dev: true
/ms@2.0.0: /ms@2.0.0:
@@ -460,6 +463,12 @@ packages:
resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==} resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==}
dev: true dev: true
/prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
hasBin: true
dev: true
/punycode@1.4.1: /punycode@1.4.1:
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
dev: true dev: true