feat(ecoos-daemon): integrate a bundled daemon web UI with components, interfaces, styles, bundling config, and server support
This commit is contained in:
202
ecoos_daemon/ts_web/elements/ecoos-displays.ts
Normal file
202
ecoos_daemon/ts_web/elements/ecoos-displays.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* EcoOS Displays View
|
||||
* Display management with enable/disable/primary controls
|
||||
*/
|
||||
|
||||
import {
|
||||
html,
|
||||
DeesElement,
|
||||
customElement,
|
||||
property,
|
||||
state,
|
||||
css,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { sharedStyles } from '../styles/shared.js';
|
||||
import type { IDisplayInfo } from '../../ts_interfaces/display.js';
|
||||
|
||||
@customElement('ecoos-displays')
|
||||
export class EcoosDisplays extends DeesElement {
|
||||
@property({ type: Array })
|
||||
public accessor displays: IDisplayInfo[] = [];
|
||||
|
||||
@state()
|
||||
private accessor loading: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
sharedStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.display-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--ecoos-border);
|
||||
}
|
||||
|
||||
.display-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.display-info {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.display-details {
|
||||
font-size: 11px;
|
||||
color: var(--ecoos-text-dim);
|
||||
}
|
||||
|
||||
.display-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.display-actions .btn {
|
||||
padding: 4px 12px;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 12px;
|
||||
color: var(--ecoos-text-dim);
|
||||
margin: 16px 0 8px 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
color: var(--ecoos-text);
|
||||
}
|
||||
|
||||
.collapsed-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsed-content.expanded {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
render(): TemplateResult {
|
||||
const enabledDisplays = this.displays.filter(d => d.active);
|
||||
const disabledDisplays = this.displays.filter(d => !d.active);
|
||||
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">Displays</div>
|
||||
|
||||
${this.displays.length === 0
|
||||
? html`<div style="color: var(--ecoos-text-dim)">No displays detected</div>`
|
||||
: html`
|
||||
<!-- Enabled Displays -->
|
||||
${enabledDisplays.map(d => this.renderDisplayItem(d))}
|
||||
|
||||
<!-- Disabled Displays -->
|
||||
${disabledDisplays.length > 0 ? html`
|
||||
<details style="margin-top: 12px;">
|
||||
<summary class="section-header">
|
||||
Disabled Displays (${disabledDisplays.length})
|
||||
</summary>
|
||||
<div style="margin-top: 8px;">
|
||||
${disabledDisplays.map(d => this.renderDisplayItem(d))}
|
||||
</div>
|
||||
</details>
|
||||
` : ''}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDisplayItem(display: IDisplayInfo): TemplateResult {
|
||||
return html`
|
||||
<div class="display-item">
|
||||
<div class="display-info">
|
||||
<div class="display-name">${display.name}</div>
|
||||
<div class="display-details">
|
||||
${display.width}x${display.height} @ ${display.refreshRate}Hz
|
||||
${display.make !== 'Unknown' ? ` • ${display.make}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="display-actions">
|
||||
${display.isPrimary
|
||||
? html`<span class="device-default">Primary</span>`
|
||||
: display.active
|
||||
? html`
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click=${() => this.setKioskDisplay(display.name)}
|
||||
?disabled=${this.loading}
|
||||
>
|
||||
Set Primary
|
||||
</button>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
<button
|
||||
class="btn ${display.active ? 'btn-danger' : 'btn-primary'}"
|
||||
@click=${() => this.toggleDisplay(display.name, !display.active)}
|
||||
?disabled=${this.loading}
|
||||
>
|
||||
${display.active ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async toggleDisplay(name: string, enable: boolean): Promise<void> {
|
||||
this.loading = true;
|
||||
try {
|
||||
const action = enable ? 'enable' : 'disable';
|
||||
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/${action}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
alert(result.message);
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
||||
} catch (error) {
|
||||
alert(`Error: ${error}`);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async setKioskDisplay(name: string): Promise<void> {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/primary`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
alert(result.message);
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
||||
} catch (error) {
|
||||
alert(`Error: ${error}`);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user