234 lines
5.9 KiB
TypeScript
234 lines
5.9 KiB
TypeScript
/**
|
||
* EcoOS Displays View
|
||
* Card-based display management
|
||
*/
|
||
|
||
import {
|
||
html,
|
||
DeesElement,
|
||
customElement,
|
||
property,
|
||
state,
|
||
css,
|
||
type TemplateResult,
|
||
} from '@design.estate/dees-element';
|
||
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;
|
||
|
||
@state()
|
||
private accessor message: string = '';
|
||
|
||
@state()
|
||
private accessor messageError: boolean = false;
|
||
|
||
public static styles = [
|
||
sharedStyles,
|
||
css`
|
||
:host {
|
||
display: block;
|
||
padding: 16px;
|
||
}
|
||
|
||
.display-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.display-card {
|
||
opacity: 1;
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
|
||
.display-card.disabled {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.display-meta {
|
||
font-size: var(--text-xs);
|
||
color: var(--text-tertiary);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.badge-row {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.actions-row {
|
||
display: flex;
|
||
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);
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 32px;
|
||
color: var(--text-tertiary);
|
||
}
|
||
|
||
.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;
|
||
gap: 6px;
|
||
}
|
||
|
||
.disabled-header::before {
|
||
content: '▶';
|
||
font-size: 8px;
|
||
transition: transform 150ms ease;
|
||
}
|
||
|
||
.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);
|
||
|
||
if (this.displays.length === 0) {
|
||
return html`
|
||
<div class="empty-state">
|
||
No displays detected
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
return html`
|
||
<div class="display-grid">
|
||
${enabledDisplays.map(d => this.renderDisplayCard(d))}
|
||
</div>
|
||
|
||
${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>
|
||
` : ''}
|
||
`;
|
||
}
|
||
|
||
private renderDisplayCard(display: IDisplayInfo): TemplateResult {
|
||
return html`
|
||
<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>
|
||
` : ''}
|
||
|
||
<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)}
|
||
></dees-button>
|
||
</div>
|
||
</dees-panel>
|
||
`;
|
||
}
|
||
|
||
private async toggleDisplay(name: string, enable: boolean): Promise<void> {
|
||
this.loading = true;
|
||
this.message = '';
|
||
try {
|
||
const action = enable ? 'enable' : 'disable';
|
||
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/${action}`, {
|
||
method: 'POST',
|
||
});
|
||
const result = await response.json();
|
||
this.message = result.message;
|
||
this.messageError = !result.success;
|
||
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
||
} catch (error) {
|
||
this.message = `Error: ${error}`;
|
||
this.messageError = true;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
|
||
private async setPrimary(name: string): Promise<void> {
|
||
this.loading = true;
|
||
this.message = '';
|
||
try {
|
||
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/primary`, {
|
||
method: 'POST',
|
||
});
|
||
const result = await response.json();
|
||
this.message = result.message;
|
||
this.messageError = !result.success;
|
||
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
||
} catch (error) {
|
||
this.message = `Error: ${error}`;
|
||
this.messageError = true;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
}
|