feat(peripherals): Add peripherals settings panel with network range management, network scanning, and manual device probe; update peripheral types and adjust UI/styling; overhaul README with expanded docs, quick start, and updated company/contact information

This commit is contained in:
2026-01-12 11:47:54 +00:00
parent fb39ff161a
commit bc170d5d75
4 changed files with 787 additions and 45 deletions

View File

@@ -31,14 +31,15 @@ export type TPeripheralCategory =
| 'power'
| 'cameras'
| 'streaming'
| 'usb';
| 'usb'
| 'settings';
export type TConnectionType = 'network' | 'usb' | 'bluetooth';
export interface IPeripheralDevice {
id: string;
name: string;
type: TPeripheralCategory;
type: Exclude<TPeripheralCategory, 'settings'>;
connectionType: TConnectionType;
status: 'online' | 'offline' | 'busy' | 'error';
ip?: string;
@@ -47,6 +48,11 @@ export interface IPeripheralDevice {
isDefault?: boolean;
}
export interface INetworkRange {
cidr: string;
label?: string;
}
@customElement('eco-view-peripherals')
export class EcoViewPeripherals extends DeesElement {
public static demo = demo;
@@ -339,15 +345,200 @@ export class EcoViewPeripherals extends DeesElement {
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 20%)')};
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
/* Header buttons */
.header-buttons {
display: flex;
align-items: center;
gap: 8px;
}
.icon-button {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
border-radius: 8px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
cursor: pointer;
transition: all 0.15s ease;
}
.icon-button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(240 5% 18%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 75%)', 'hsl(240 5% 35%)')};
}
/* Settings panel styles */
.settings-section {
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.settings-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
}
.settings-description {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
margin-bottom: 16px;
}
.network-input-group {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.network-input {
flex: 1;
padding: 10px 14px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
border-radius: 8px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
font-size: 14px;
font-family: ui-monospace, monospace;
}
.network-input::placeholder {
color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 45%)')};
}
.network-input:focus {
outline: none;
border-color: hsl(217 91% 60%);
}
.add-button {
padding: 10px 16px;
background: hsl(217 91% 60%);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: background 0.15s ease;
}
.add-button:hover {
background: hsl(217 91% 55%);
}
.network-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.network-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')};
border-radius: 8px;
}
.network-item-info {
display: flex;
align-items: center;
gap: 12px;
}
.network-item-icon {
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.network-item-cidr {
font-family: ui-monospace, monospace;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
}
.network-item-label {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
}
.remove-button {
padding: 6px;
background: transparent;
border: none;
border-radius: 6px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
cursor: pointer;
transition: all 0.15s ease;
}
.remove-button:hover {
background: ${cssManager.bdTheme('hsl(0 72% 95%)', 'hsl(0 72% 30% / 0.2)')};
color: hsl(0 72% 51%);
}
.scan-networks-button {
margin-top: 16px;
padding: 12px 20px;
background: hsl(142 71% 45%);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.15s ease;
}
.scan-networks-button:hover {
background: hsl(142 71% 40%);
}
.scan-networks-button:disabled {
background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
cursor: not-allowed;
}
.empty-networks {
padding: 24px;
text-align: center;
color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')};
font-size: 14px;
}
`,
];
@property({ type: String })
accessor activeCategory: TPeripheralCategory = 'all';
@property({ type: Array })
accessor networkRanges: INetworkRange[] = [];
@state()
accessor isScanning = false;
@state()
accessor newNetworkInput = '';
@state()
accessor newDeviceIpInput = '';
@state()
accessor devices: IPeripheralDevice[] = [
// Mock printers
@@ -577,6 +768,17 @@ export class EcoViewPeripherals extends DeesElement {
},
],
},
{
name: 'Configuration',
iconName: 'lucide:settings',
items: [
{
key: 'settings',
iconName: 'lucide:network',
action: () => this.activeCategory = 'settings',
},
],
},
];
}
@@ -609,6 +811,7 @@ export class EcoViewPeripherals extends DeesElement {
cameras: 'Cameras',
streaming: 'Streaming Devices',
usb: 'USB Devices',
settings: 'Network Settings',
};
return titles[this.activeCategory];
}
@@ -624,12 +827,13 @@ export class EcoViewPeripherals extends DeesElement {
cameras: 'Webcams and security cameras',
streaming: 'Apple TV, Chromecast, and streaming devices',
usb: 'USB storage and connected devices',
settings: 'Configure network ranges and add devices manually',
};
return descriptions[this.activeCategory];
}
private getDeviceIcon(device: IPeripheralDevice): string {
const icons: Record<TPeripheralCategory, string> = {
const icons: Record<Exclude<TPeripheralCategory, 'settings'>, string> = {
all: 'lucide:monitor',
printers: 'lucide:printer',
scanners: 'lucide:scan',
@@ -692,6 +896,74 @@ export class EcoViewPeripherals extends DeesElement {
}));
}
private handleAddNetwork(): void {
const cidr = this.newNetworkInput.trim();
if (!cidr) return;
// Validate CIDR format (basic validation)
const cidrRegex = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/;
if (!cidrRegex.test(cidr)) {
// Could show error, for now just return
return;
}
// Check for duplicates
if (this.networkRanges.some(r => r.cidr === cidr)) {
this.newNetworkInput = '';
return;
}
this.networkRanges = [...this.networkRanges, { cidr }];
this.newNetworkInput = '';
this.dispatchEvent(new CustomEvent('networks-change', {
detail: { networks: this.networkRanges },
bubbles: true,
composed: true,
}));
}
private handleRemoveNetwork(cidr: string): void {
this.networkRanges = this.networkRanges.filter(r => r.cidr !== cidr);
this.dispatchEvent(new CustomEvent('networks-change', {
detail: { networks: this.networkRanges },
bubbles: true,
composed: true,
}));
}
private handleAddDeviceByIp(): void {
const ip = this.newDeviceIpInput.trim();
if (!ip) return;
// Validate IP format
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!ipRegex.test(ip)) {
return;
}
this.newDeviceIpInput = '';
// Dispatch event to scan this specific IP
this.dispatchEvent(new CustomEvent('scan-ip', {
detail: { ip },
bubbles: true,
composed: true,
}));
}
private handleScanNetworks(): void {
if (this.isScanning || this.networkRanges.length === 0) return;
this.isScanning = true;
this.dispatchEvent(new CustomEvent('scan-networks', {
detail: { networks: this.networkRanges.map(r => r.cidr) },
bubbles: true,
composed: true,
}));
}
public render(): TemplateResult {
return html`
<div class="peripherals-container">
@@ -708,6 +980,10 @@ export class EcoViewPeripherals extends DeesElement {
}
private renderContent(): TemplateResult {
if (this.activeCategory === 'settings') {
return this.renderSettings();
}
const devices = this.getFilteredDevices();
return html`
@@ -716,14 +992,23 @@ export class EcoViewPeripherals extends DeesElement {
<div class="panel-title">${this.getCategoryTitle()}</div>
<div class="panel-description">${this.getCategoryDescription()}</div>
</div>
<button
class="scan-button ${this.isScanning ? 'scanning' : ''}"
@click=${this.handleScan}
?disabled=${this.isScanning}
>
<dees-icon .icon=${this.isScanning ? 'lucide:loader2' : 'lucide:radar'} .iconSize=${16}></dees-icon>
${this.isScanning ? 'Scanning...' : 'Scan for Devices'}
</button>
<div class="header-buttons">
<button
class="icon-button"
@click=${() => this.activeCategory = 'settings'}
title="Network Settings"
>
<dees-icon .icon=${'lucide:settings'} .iconSize=${18}></dees-icon>
</button>
<button
class="scan-button ${this.isScanning ? 'scanning' : ''}"
@click=${this.handleScan}
?disabled=${this.isScanning}
>
<dees-icon .icon=${this.isScanning ? 'lucide:loader2' : 'lucide:radar'} .iconSize=${16}></dees-icon>
${this.isScanning ? 'Scanning...' : 'Scan for Devices'}
</button>
</div>
</div>
${this.activeCategory === 'all'
@@ -733,6 +1018,93 @@ export class EcoViewPeripherals extends DeesElement {
`;
}
private renderSettings(): TemplateResult {
return html`
<div class="panel-header">
<div class="panel-header-left">
<div class="panel-title">${this.getCategoryTitle()}</div>
<div class="panel-description">${this.getCategoryDescription()}</div>
</div>
</div>
<!-- Network Ranges Section -->
<div class="settings-section">
<div class="settings-title">Network Ranges</div>
<div class="settings-description">
Add network ranges in CIDR notation to scan for devices (e.g., 192.168.1.0/24)
</div>
<div class="network-input-group">
<input
type="text"
class="network-input"
placeholder="192.168.1.0/24"
.value=${this.newNetworkInput}
@input=${(e: InputEvent) => this.newNetworkInput = (e.target as HTMLInputElement).value}
@keydown=${(e: KeyboardEvent) => e.key === 'Enter' && this.handleAddNetwork()}
/>
<button class="add-button" @click=${this.handleAddNetwork}>
<dees-icon .icon=${'lucide:plus'} .iconSize=${16}></dees-icon>
Add
</button>
</div>
${this.networkRanges.length > 0 ? html`
<div class="network-list">
${this.networkRanges.map(range => html`
<div class="network-item">
<div class="network-item-info">
<dees-icon class="network-item-icon" .icon=${'lucide:network'} .iconSize=${18}></dees-icon>
<span class="network-item-cidr">${range.cidr}</span>
${range.label ? html`<span class="network-item-label">${range.label}</span>` : ''}
</div>
<button class="remove-button" @click=${() => this.handleRemoveNetwork(range.cidr)}>
<dees-icon .icon=${'lucide:x'} .iconSize=${16}></dees-icon>
</button>
</div>
`)}
</div>
<button
class="scan-networks-button"
@click=${this.handleScanNetworks}
?disabled=${this.isScanning}
>
<dees-icon .icon=${this.isScanning ? 'lucide:loader2' : 'lucide:radar'} .iconSize=${16}></dees-icon>
${this.isScanning ? 'Scanning...' : 'Scan All Networks'}
</button>
` : html`
<div class="empty-networks">
No network ranges configured. Add a range above to enable network scanning.
</div>
`}
</div>
<!-- Add Device by IP Section -->
<div class="settings-section">
<div class="settings-title">Add Device by IP</div>
<div class="settings-description">
Add a specific device by entering its IP address directly
</div>
<div class="network-input-group">
<input
type="text"
class="network-input"
placeholder="192.168.1.100"
.value=${this.newDeviceIpInput}
@input=${(e: InputEvent) => this.newDeviceIpInput = (e.target as HTMLInputElement).value}
@keydown=${(e: KeyboardEvent) => e.key === 'Enter' && this.handleAddDeviceByIp()}
/>
<button class="add-button" @click=${this.handleAddDeviceByIp}>
<dees-icon .icon=${'lucide:plus'} .iconSize=${16}></dees-icon>
Probe Device
</button>
</div>
</div>
`;
}
private renderGroupedDevices(devices: IPeripheralDevice[]): TemplateResult {
const groups = new Map<TPeripheralCategory, IPeripheralDevice[]>();
@@ -742,7 +1114,7 @@ export class EcoViewPeripherals extends DeesElement {
groups.set(device.type, existing);
}
const categoryLabels: Record<TPeripheralCategory, string> = {
const categoryLabels: Record<Exclude<TPeripheralCategory, 'settings'>, string> = {
all: 'All',
printers: 'Printers',
scanners: 'Scanners',