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

@ -53,7 +53,7 @@
Poll delay adds a user configurable delay (in seconds) to each round of Poll delay adds a user configurable delay (in seconds) to each round of
status updates of all item in the device's menu that might be amended status updates of all item in the device's menu that might be amended
externally from the watch. A user has requested that it is possible to add externally from the watch. A user has requested that it is possible to add
this delayfor an "always open" mode of operation, which then drains the this delay for an "always open" mode of operation, which then drains the
watch battery from the additional API access activity. watch battery from the additional API access activity.
--> -->
<property id="poll_delay_combined" type="number">5</property> <property id="poll_delay_combined" type="number">5</property>

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