Files
catalog/ts_web/views/eco-view-peripherals/eco-view-peripherals.ts

1213 lines
33 KiB
TypeScript
Raw Normal View History

2026-01-06 10:17:05 +00:00
import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
2026-01-12 10:57:54 +00:00
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
import { demo } from './eco-view-peripherals.demo.js';
2026-01-06 10:17:05 +00:00
// Ensure components are registered
DeesAppuiSecondarymenu;
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
2026-01-12 10:57:54 +00:00
'eco-view-peripherals': EcoViewPeripherals;
2026-01-06 10:17:05 +00:00
}
}
export type TPeripheralCategory =
| 'all'
| 'printers'
| 'scanners'
| 'speakers'
| 'storage'
| 'power'
| 'cameras'
| 'streaming'
| 'usb'
| 'settings';
2026-01-06 10:17:05 +00:00
export type TConnectionType = 'network' | 'usb' | 'bluetooth';
export interface IPeripheralDevice {
id: string;
name: string;
type: Exclude<TPeripheralCategory, 'settings'>;
2026-01-06 10:17:05 +00:00
connectionType: TConnectionType;
status: 'online' | 'offline' | 'busy' | 'error';
ip?: string;
manufacturer?: string;
model?: string;
isDefault?: boolean;
}
export interface INetworkRange {
cidr: string;
label?: string;
}
2026-01-12 10:57:54 +00:00
@customElement('eco-view-peripherals')
export class EcoViewPeripherals extends DeesElement {
2026-01-06 10:17:05 +00:00
public static demo = demo;
2026-01-12 10:57:54 +00:00
public static demoGroup = 'Views';
2026-01-06 10:17:05 +00:00
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')};
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.peripherals-container {
display: flex;
height: 100%;
}
dees-appui-secondarymenu {
flex-shrink: 0;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')};
}
.content {
flex: 1;
overflow-y: auto;
padding: 32px 48px;
}
.panel-header {
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-header-left {
display: flex;
flex-direction: column;
gap: 4px;
}
.panel-title {
font-size: 28px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
}
.panel-description {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.scan-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: hsl(217 91% 60%);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease;
}
.scan-button:hover {
background: hsl(217 91% 55%);
}
.scan-button.scanning {
background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
cursor: not-allowed;
}
.scan-button.scanning dees-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.device-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;
margin-bottom: 24px;
overflow: hidden;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
}
.section-title {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
text-transform: uppercase;
letter-spacing: 0.5px;
}
.device-count {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 20%)')};
padding: 2px 8px;
border-radius: 10px;
}
.device-list {
padding: 8px 0;
}
.device-item {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 20px;
cursor: pointer;
transition: background 0.15s ease;
}
.device-item:hover {
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')};
}
.device-icon-wrapper {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
}
.device-icon-wrapper.online {
background: ${cssManager.bdTheme('hsl(142 71% 93%)', 'hsl(142 71% 45% / 0.15)')};
color: hsl(142 71% 35%);
}
.device-icon-wrapper.busy {
background: ${cssManager.bdTheme('hsl(47 100% 93%)', 'hsl(47 100% 50% / 0.15)')};
color: hsl(47 100% 40%);
}
.device-icon-wrapper.error {
background: ${cssManager.bdTheme('hsl(0 72% 93%)', 'hsl(0 72% 51% / 0.15)')};
color: hsl(0 72% 45%);
}
.device-info {
flex: 1;
min-width: 0;
}
.device-name {
font-size: 15px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
display: flex;
align-items: center;
gap: 8px;
}
.default-badge {
font-size: 10px;
font-weight: 600;
color: hsl(217 91% 60%);
background: ${cssManager.bdTheme('hsl(217 91% 95%)', 'hsl(217 91% 60% / 0.15)')};
padding: 2px 6px;
border-radius: 4px;
text-transform: uppercase;
}
.device-details {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
margin-top: 2px;
display: flex;
align-items: center;
gap: 12px;
}
.device-detail {
display: flex;
align-items: center;
gap: 4px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-indicator.online {
background: hsl(142 71% 45%);
}
.status-indicator.offline {
background: ${cssManager.bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 40%)')};
}
.status-indicator.busy {
background: hsl(47 100% 50%);
}
.status-indicator.error {
background: hsl(0 72% 51%);
}
.device-actions {
display: flex;
align-items: center;
gap: 8px;
}
.action-button {
padding: 8px 12px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
border-radius: 6px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
font-size: 13px;
cursor: pointer;
transition: all 0.15s ease;
}
.action-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%)')};
}
.action-button.primary {
background: hsl(217 91% 60%);
border-color: hsl(217 91% 60%);
color: white;
}
.action-button.primary:hover {
background: hsl(217 91% 55%);
}
.empty-state {
padding: 48px 20px;
text-align: center;
}
.empty-icon {
color: ${cssManager.bdTheme('hsl(0 0% 75%)', 'hsl(0 0% 35%)')};
margin-bottom: 16px;
}
.empty-title {
font-size: 16px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
margin-bottom: 8px;
}
.empty-description {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')};
}
.connection-type {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
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;
}
2026-01-06 10:17:05 +00:00
`,
];
@property({ type: String })
accessor activeCategory: TPeripheralCategory = 'all';
@property({ type: Array })
accessor networkRanges: INetworkRange[] = [];
2026-01-06 10:17:05 +00:00
@state()
accessor isScanning = false;
@state()
accessor newNetworkInput = '';
@state()
accessor newDeviceIpInput = '';
2026-01-06 10:17:05 +00:00
@state()
accessor devices: IPeripheralDevice[] = [
// Mock printers
{
id: 'printer-1',
name: 'HP LaserJet Pro',
type: 'printers',
connectionType: 'network',
status: 'online',
ip: '192.168.1.50',
manufacturer: 'HP',
model: 'LaserJet Pro M404n',
isDefault: true,
},
{
id: 'printer-2',
name: 'Brother MFC-L2750DW',
type: 'printers',
connectionType: 'network',
status: 'online',
ip: '192.168.1.51',
manufacturer: 'Brother',
model: 'MFC-L2750DW',
},
{
id: 'printer-3',
name: 'Canon PIXMA',
type: 'printers',
connectionType: 'usb',
status: 'offline',
manufacturer: 'Canon',
model: 'PIXMA TR8620',
},
// Mock scanners
{
id: 'scanner-1',
name: 'Epson Perfection V600',
type: 'scanners',
connectionType: 'usb',
status: 'online',
manufacturer: 'Epson',
model: 'Perfection V600',
},
// Mock speakers
{
id: 'speaker-1',
name: 'Sonos One',
type: 'speakers',
connectionType: 'network',
status: 'online',
ip: '192.168.1.60',
manufacturer: 'Sonos',
model: 'One (Gen 2)',
},
{
id: 'speaker-2',
name: 'HomePod mini',
type: 'speakers',
connectionType: 'network',
status: 'online',
ip: '192.168.1.61',
manufacturer: 'Apple',
model: 'HomePod mini',
},
// Mock NAS
{
id: 'nas-1',
name: 'Synology DS920+',
type: 'storage',
connectionType: 'network',
status: 'online',
ip: '192.168.1.100',
manufacturer: 'Synology',
model: 'DS920+',
},
// Mock UPS
{
id: 'ups-1',
name: 'APC Back-UPS Pro',
type: 'power',
connectionType: 'usb',
status: 'online',
manufacturer: 'APC',
model: 'Back-UPS Pro 1500',
},
// Mock cameras
{
id: 'camera-1',
name: 'Logitech C920',
type: 'cameras',
connectionType: 'usb',
status: 'online',
manufacturer: 'Logitech',
model: 'C920 HD Pro',
isDefault: true,
},
{
id: 'camera-2',
name: 'Ring Indoor Cam',
type: 'cameras',
connectionType: 'network',
status: 'online',
ip: '192.168.1.70',
manufacturer: 'Ring',
model: 'Indoor Cam',
},
// Mock streaming devices
{
id: 'streaming-1',
name: 'Living Room Apple TV',
type: 'streaming',
connectionType: 'network',
status: 'online',
ip: '192.168.1.80',
manufacturer: 'Apple',
model: 'Apple TV 4K',
},
{
id: 'streaming-2',
name: 'Bedroom Chromecast',
type: 'streaming',
connectionType: 'network',
status: 'online',
ip: '192.168.1.81',
manufacturer: 'Google',
model: 'Chromecast with Google TV',
},
// Mock USB devices
{
id: 'usb-1',
name: 'SanDisk Ultra',
type: 'usb',
connectionType: 'usb',
status: 'online',
manufacturer: 'SanDisk',
model: 'Ultra USB 3.0 128GB',
},
];
private getMenuGroups(): ISecondaryMenuGroup[] {
const allCount = this.devices.length;
const getCount = (type: TPeripheralCategory) =>
this.devices.filter(d => d.type === type).length;
return [
{
name: 'Devices',
iconName: 'lucide:monitor',
items: [
{
key: 'all',
iconName: 'lucide:layoutGrid',
action: () => this.activeCategory = 'all',
badge: allCount,
},
],
},
{
name: 'Output',
iconName: 'lucide:printer',
items: [
{
key: 'printers',
iconName: 'lucide:printer',
action: () => this.activeCategory = 'printers',
badge: getCount('printers') || undefined,
},
{
key: 'speakers',
iconName: 'lucide:speaker',
action: () => this.activeCategory = 'speakers',
badge: getCount('speakers') || undefined,
},
],
},
{
name: 'Input',
iconName: 'lucide:scan',
items: [
{
key: 'scanners',
iconName: 'lucide:scan',
action: () => this.activeCategory = 'scanners',
badge: getCount('scanners') || undefined,
},
{
key: 'cameras',
iconName: 'lucide:camera',
action: () => this.activeCategory = 'cameras',
badge: getCount('cameras') || undefined,
},
],
},
{
name: 'Network',
iconName: 'lucide:network',
items: [
{
key: 'storage',
iconName: 'lucide:hardDrive',
action: () => this.activeCategory = 'storage',
badge: getCount('storage') || undefined,
},
{
key: 'streaming',
iconName: 'lucide:cast',
action: () => this.activeCategory = 'streaming',
badge: getCount('streaming') || undefined,
},
],
},
{
name: 'Other',
iconName: 'lucide:plug',
items: [
{
key: 'power',
iconName: 'lucide:batteryCharging',
action: () => this.activeCategory = 'power',
badge: getCount('power') || undefined,
},
{
key: 'usb',
iconName: 'lucide:usb',
action: () => this.activeCategory = 'usb',
badge: getCount('usb') || undefined,
},
],
},
{
name: 'Configuration',
iconName: 'lucide:settings',
items: [
{
key: 'settings',
iconName: 'lucide:network',
action: () => this.activeCategory = 'settings',
},
],
},
2026-01-06 10:17:05 +00:00
];
}
private getSelectedItem(): ISecondaryMenuItem | null {
for (const group of this.getMenuGroups()) {
for (const item of group.items) {
if ('key' in item && item.key === this.activeCategory) {
return item;
}
}
}
return null;
}
private getFilteredDevices(): IPeripheralDevice[] {
if (this.activeCategory === 'all') {
return this.devices;
}
return this.devices.filter(d => d.type === this.activeCategory);
}
private getCategoryTitle(): string {
const titles: Record<TPeripheralCategory, string> = {
all: 'All Devices',
printers: 'Printers',
scanners: 'Scanners',
speakers: 'Speakers',
storage: 'Network Storage',
power: 'Power Devices',
cameras: 'Cameras',
streaming: 'Streaming Devices',
usb: 'USB Devices',
settings: 'Network Settings',
2026-01-06 10:17:05 +00:00
};
return titles[this.activeCategory];
}
private getCategoryDescription(): string {
const descriptions: Record<TPeripheralCategory, string> = {
all: 'View and manage all connected peripherals',
printers: 'Manage printers and print queues',
scanners: 'Configure scanners and scanning options',
speakers: 'Network speakers and audio devices',
storage: 'NAS devices and network storage',
power: 'UPS and power management devices',
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',
2026-01-06 10:17:05 +00:00
};
return descriptions[this.activeCategory];
}
private getDeviceIcon(device: IPeripheralDevice): string {
const icons: Record<Exclude<TPeripheralCategory, 'settings'>, string> = {
2026-01-06 10:17:05 +00:00
all: 'lucide:monitor',
printers: 'lucide:printer',
scanners: 'lucide:scan',
speakers: 'lucide:speaker',
storage: 'lucide:hardDrive',
power: 'lucide:batteryCharging',
cameras: 'lucide:camera',
streaming: 'lucide:cast',
usb: 'lucide:usb',
};
return icons[device.type];
}
private getConnectionIcon(type: TConnectionType): string {
const icons: Record<TConnectionType, string> = {
network: 'lucide:wifi',
usb: 'lucide:usb',
bluetooth: 'lucide:bluetooth',
};
return icons[type];
}
private async handleScan(): Promise<void> {
if (this.isScanning) return;
this.isScanning = true;
this.dispatchEvent(new CustomEvent('scan-start', {
bubbles: true,
composed: true,
}));
// Simulate scanning
await new Promise(resolve => setTimeout(resolve, 3000));
this.isScanning = false;
this.dispatchEvent(new CustomEvent('scan-complete', {
bubbles: true,
composed: true,
}));
}
private handleDeviceClick(device: IPeripheralDevice): void {
this.dispatchEvent(new CustomEvent('device-select', {
detail: { device },
bubbles: true,
composed: true,
}));
}
private handleSetDefault(device: IPeripheralDevice, e: Event): void {
e.stopPropagation();
this.devices = this.devices.map(d => ({
...d,
isDefault: d.type === device.type ? d.id === device.id : d.isDefault,
}));
this.dispatchEvent(new CustomEvent('device-set-default', {
detail: { device },
bubbles: true,
composed: true,
}));
}
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,
}));
}
2026-01-06 10:17:05 +00:00
public render(): TemplateResult {
return html`
<div class="peripherals-container">
<dees-appui-secondarymenu
.heading=${'Peripherals'}
.groups=${this.getMenuGroups()}
.selectedItem=${this.getSelectedItem()}
></dees-appui-secondarymenu>
<div class="content">
${this.renderContent()}
</div>
</div>
`;
}
private renderContent(): TemplateResult {
if (this.activeCategory === 'settings') {
return this.renderSettings();
}
2026-01-06 10:17:05 +00:00
const devices = this.getFilteredDevices();
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 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>
2026-01-06 10:17:05 +00:00
</div>
${this.activeCategory === 'all'
? this.renderGroupedDevices(devices)
: this.renderDeviceList(devices)
}
`;
}
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>
`;
}
2026-01-06 10:17:05 +00:00
private renderGroupedDevices(devices: IPeripheralDevice[]): TemplateResult {
const groups = new Map<TPeripheralCategory, IPeripheralDevice[]>();
for (const device of devices) {
const existing = groups.get(device.type) || [];
existing.push(device);
groups.set(device.type, existing);
}
const categoryLabels: Record<Exclude<TPeripheralCategory, 'settings'>, string> = {
2026-01-06 10:17:05 +00:00
all: 'All',
printers: 'Printers',
scanners: 'Scanners',
speakers: 'Speakers',
storage: 'Network Storage',
power: 'Power Devices',
cameras: 'Cameras',
streaming: 'Streaming',
usb: 'USB Devices',
};
return html`
${Array.from(groups.entries()).map(([category, categoryDevices]) => html`
<div class="device-section">
<div class="section-header">
<span class="section-title">${categoryLabels[category]}</span>
<span class="device-count">${categoryDevices.length}</span>
</div>
<div class="device-list">
${categoryDevices.map(device => this.renderDeviceItem(device))}
</div>
</div>
`)}
`;
}
private renderDeviceList(devices: IPeripheralDevice[]): TemplateResult {
if (devices.length === 0) {
return this.renderEmptyState();
}
return html`
<div class="device-section">
<div class="section-header">
<span class="section-title">Discovered Devices</span>
<span class="device-count">${devices.length}</span>
</div>
<div class="device-list">
${devices.map(device => this.renderDeviceItem(device))}
</div>
</div>
`;
}
private renderDeviceItem(device: IPeripheralDevice): TemplateResult {
return html`
<div class="device-item" @click=${() => this.handleDeviceClick(device)}>
<div class="device-icon-wrapper ${device.status}">
<dees-icon .icon=${this.getDeviceIcon(device)} .iconSize=${24}></dees-icon>
</div>
<div class="device-info">
<div class="device-name">
${device.name}
${device.isDefault ? html`<span class="default-badge">Default</span>` : ''}
</div>
<div class="device-details">
<div class="device-detail">
<span class="status-indicator ${device.status}"></span>
${device.status.charAt(0).toUpperCase() + device.status.slice(1)}
</div>
${device.ip ? html`
<div class="device-detail">${device.ip}</div>
` : ''}
<span class="connection-type">
<dees-icon .icon=${this.getConnectionIcon(device.connectionType)} .iconSize=${10}></dees-icon>
${device.connectionType.toUpperCase()}
</span>
</div>
</div>
<div class="device-actions">
${!device.isDefault && (device.type === 'printers' || device.type === 'cameras') ? html`
<button class="action-button" @click=${(e: Event) => this.handleSetDefault(device, e)}>
Set Default
</button>
` : ''}
<dees-icon .icon=${'lucide:chevronRight'} .iconSize=${16}></dees-icon>
</div>
</div>
`;
}
private renderEmptyState(): TemplateResult {
return html`
<div class="device-section">
<div class="empty-state">
<dees-icon class="empty-icon" .icon=${'lucide:searchX'} .iconSize=${48}></dees-icon>
<div class="empty-title">No devices found</div>
<div class="empty-description">
Click "Scan for Devices" to discover ${this.getCategoryTitle().toLowerCase()} on your network or connected via USB.
</div>
</div>
</div>
`;
}
}