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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user