2026-01-12 01:51:22 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* EcoOS Displays View
|
2026-01-12 14:34:56 +00:00
|
|
|
|
* Card-based display management
|
2026-01-12 01:51:22 +00:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
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';
|
2026-01-12 01:51:22 +00:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-01-12 01:51:22 +00:00
|
|
|
|
public static styles = [
|
|
|
|
|
|
sharedStyles,
|
|
|
|
|
|
css`
|
|
|
|
|
|
:host {
|
|
|
|
|
|
display: block;
|
2026-01-12 14:34:56 +00:00
|
|
|
|
padding: 16px;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
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 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 14:34:56 +00:00
|
|
|
|
.display-card {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transition: opacity 0.2s ease;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 14:34:56 +00:00
|
|
|
|
.display-card.disabled {
|
|
|
|
|
|
opacity: 0.5;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
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 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 14:34:56 +00:00
|
|
|
|
.badge-row {
|
|
|
|
|
|
margin-bottom: 12px;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 14:34:56 +00:00
|
|
|
|
.actions-row {
|
2026-01-12 01:51:22 +00:00
|
|
|
|
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 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 14:34:56 +00:00
|
|
|
|
.empty-state {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 32px;
|
|
|
|
|
|
color: var(--text-tertiary);
|
2026-01-12 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-12 14:34:56 +00:00
|
|
|
|
gap: 6px;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 14:34:56 +00:00
|
|
|
|
.disabled-header::before {
|
|
|
|
|
|
content: '▶';
|
|
|
|
|
|
font-size: 8px;
|
|
|
|
|
|
transition: transform 150ms ease;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 14:34:56 +00:00
|
|
|
|
.disabled-header.open::before {
|
|
|
|
|
|
transform: rotate(90deg);
|
2026-01-12 01:51:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
`,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 01:51:22 +00:00
|
|
|
|
return html`
|
2026-01-12 14:34:56 +00:00
|
|
|
|
<div class="display-grid">
|
|
|
|
|
|
${enabledDisplays.map(d => this.renderDisplayCard(d))}
|
2026-01-12 01:51:22 +00:00
|
|
|
|
</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 01:51:22 +00:00
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 14:34:56 +00:00
|
|
|
|
private renderDisplayCard(display: IDisplayInfo): TemplateResult {
|
2026-01-12 01:51:22 +00:00
|
|
|
|
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>
|
2026-01-12 01:51:22 +00:00
|
|
|
|
</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}
|
2026-01-12 01:51:22 +00:00
|
|
|
|
@click=${() => this.toggleDisplay(display.name, !display.active)}
|
2026-01-12 14:34:56 +00:00
|
|
|
|
></dees-button>
|
2026-01-12 01:51:22 +00:00
|
|
|
|
</div>
|
2026-01-12 14:34:56 +00:00
|
|
|
|
</dees-panel>
|
2026-01-12 01:51:22 +00:00
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async toggleDisplay(name: string, enable: boolean): Promise<void> {
|
|
|
|
|
|
this.loading = true;
|
2026-01-12 14:34:56 +00:00
|
|
|
|
this.message = '';
|
2026-01-12 01:51:22 +00:00
|
|
|
|
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;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
|
|
|
|
|
} catch (error) {
|
2026-01-12 14:34:56 +00:00
|
|
|
|
this.message = `Error: ${error}`;
|
|
|
|
|
|
this.messageError = true;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 14:34:56 +00:00
|
|
|
|
private async setPrimary(name: string): Promise<void> {
|
2026-01-12 01:51:22 +00:00
|
|
|
|
this.loading = true;
|
2026-01-12 14:34:56 +00:00
|
|
|
|
this.message = '';
|
2026-01-12 01:51:22 +00:00
|
|
|
|
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;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
|
|
|
|
|
} catch (error) {
|
2026-01-12 14:34:56 +00:00
|
|
|
|
this.message = `Error: ${error}`;
|
|
|
|
|
|
this.messageError = true;
|
2026-01-12 01:51:22 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|