203 lines
5.1 KiB
TypeScript
203 lines
5.1 KiB
TypeScript
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|