Files
eco_os/ecoos_daemon/ts_web/elements/ecoos-displays.ts

234 lines
5.9 KiB
TypeScript
Raw Normal View History

/**
* EcoOS Displays View
2026-01-12 14:34:56 +00:00
* Card-based display management
*/
import {
html,
DeesElement,
customElement,
property,
state,
css,
type TemplateResult,
} from '@design.estate/dees-element';
2026-01-12 14:34:56 +00:00
import { DeesButton, DeesPanel, DeesBadge } from '@design.estate/dees-catalog';
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;
2026-01-12 14:34:56 +00:00
@state()
private accessor message: string = '';
@state()
private accessor messageError: boolean = false;
public static styles = [
sharedStyles,
css`
:host {
display: block;
2026-01-12 14:34:56 +00:00
padding: 16px;
}
2026-01-12 14:34:56 +00:00
.display-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
2026-01-12 14:34:56 +00:00
.display-card {
opacity: 1;
transition: opacity 0.2s ease;
}
2026-01-12 14:34:56 +00:00
.display-card.disabled {
opacity: 0.5;
}
2026-01-12 14:34:56 +00:00
.display-meta {
font-size: var(--text-xs);
color: var(--text-tertiary);
margin-bottom: 12px;
}
2026-01-12 14:34:56 +00:00
.badge-row {
margin-bottom: 12px;
}
2026-01-12 14:34:56 +00:00
.actions-row {
display: flex;
2026-01-12 14:34:56 +00:00
gap: 8px;
flex-wrap: wrap;
}
.message-bar {
margin-top: 16px;
padding: 8px 12px;
border-radius: 6px;
font-size: var(--text-sm);
}
.message-bar.success {
background: hsla(142.1, 76.2%, 36.3%, 0.15);
color: var(--success);
}
.message-bar.error {
background: hsla(0, 84.2%, 60.2%, 0.15);
color: var(--error);
}
2026-01-12 14:34:56 +00:00
.empty-state {
text-align: center;
padding: 32px;
color: var(--text-tertiary);
}
2026-01-12 14:34:56 +00:00
.disabled-section {
margin-top: 16px;
}
.disabled-header {
font-size: var(--text-sm);
color: var(--text-tertiary);
margin-bottom: 12px;
cursor: pointer;
display: flex;
align-items: center;
2026-01-12 14:34:56 +00:00
gap: 6px;
}
2026-01-12 14:34:56 +00:00
.disabled-header::before {
content: '▶';
font-size: 8px;
transition: transform 150ms ease;
}
2026-01-12 14:34:56 +00:00
.disabled-header.open::before {
transform: rotate(90deg);
}
`,
];
render(): TemplateResult {
const enabledDisplays = this.displays.filter(d => d.active);
const disabledDisplays = this.displays.filter(d => !d.active);
2026-01-12 14:34:56 +00:00
if (this.displays.length === 0) {
return html`
<div class="empty-state">
No displays detected
</div>
`;
}
return html`
2026-01-12 14:34:56 +00:00
<div class="display-grid">
${enabledDisplays.map(d => this.renderDisplayCard(d))}
</div>
2026-01-12 14:34:56 +00:00
${disabledDisplays.length > 0 ? html`
<details class="disabled-section">
<summary class="disabled-header">
Disabled Displays (${disabledDisplays.length})
</summary>
<div class="display-grid" style="margin-top: 12px;">
${disabledDisplays.map(d => this.renderDisplayCard(d))}
</div>
</details>
` : ''}
${this.message ? html`
<div class="message-bar ${this.messageError ? 'error' : 'success'}">${this.message}</div>
` : ''}
`;
}
2026-01-12 14:34:56 +00:00
private renderDisplayCard(display: IDisplayInfo): TemplateResult {
return html`
2026-01-12 14:34:56 +00:00
<dees-panel
class="display-card ${display.active ? '' : 'disabled'}"
.title=${display.name}
.subtitle=${`${display.width}×${display.height} @ ${display.refreshRate}Hz`}
.variant=${display.active ? 'default' : 'ghost'}
>
${display.make && display.make !== 'Unknown' ? html`
<div class="display-meta">${display.make}${display.model ? ` ${display.model}` : ''}</div>
` : ''}
${display.isPrimary ? html`
<div class="badge-row">
<dees-badge .type=${'primary'}>Primary</dees-badge>
</div>
2026-01-12 14:34:56 +00:00
` : ''}
<div class="actions-row">
${display.active && !display.isPrimary ? html`
<dees-button
.type=${'default'}
.text=${'Set Primary'}
.disabled=${this.loading}
@click=${() => this.setPrimary(display.name)}
></dees-button>
` : ''}
<dees-button
.type=${'default'}
.status=${display.active ? 'error' : 'success'}
.text=${display.active ? 'Disable' : 'Enable'}
.disabled=${this.loading}
@click=${() => this.toggleDisplay(display.name, !display.active)}
2026-01-12 14:34:56 +00:00
></dees-button>
</div>
2026-01-12 14:34:56 +00:00
</dees-panel>
`;
}
private async toggleDisplay(name: string, enable: boolean): Promise<void> {
this.loading = true;
2026-01-12 14:34:56 +00:00
this.message = '';
try {
const action = enable ? 'enable' : 'disable';
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/${action}`, {
method: 'POST',
});
const result = await response.json();
2026-01-12 14:34:56 +00:00
this.message = result.message;
this.messageError = !result.success;
this.dispatchEvent(new CustomEvent('refresh-displays'));
} catch (error) {
2026-01-12 14:34:56 +00:00
this.message = `Error: ${error}`;
this.messageError = true;
} finally {
this.loading = false;
}
}
2026-01-12 14:34:56 +00:00
private async setPrimary(name: string): Promise<void> {
this.loading = true;
2026-01-12 14:34:56 +00:00
this.message = '';
try {
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/primary`, {
method: 'POST',
});
const result = await response.json();
2026-01-12 14:34:56 +00:00
this.message = result.message;
this.messageError = !result.success;
this.dispatchEvent(new CustomEvent('refresh-displays'));
} catch (error) {
2026-01-12 14:34:56 +00:00
this.message = `Error: ${error}`;
this.messageError = true;
} finally {
this.loading = false;
}
}
}