Port the HLCRF application frame from lthn-desktop into core/gui/ui/ as a reusable Angular framework. Adds: - ApplicationFrameComponent: header, collapsible sidebar, content area, footer - SystemTrayFrameComponent: 380x480 frameless panel with provider status cards - ProviderDiscoveryService: fetches GET /api/v1/providers, loads custom elements - ProviderHostComponent: renders any custom element by tag via Renderer2 - ProviderNavComponent: dynamic sidebar navigation from provider discovery - StatusBarComponent: footer with time, version, provider count, WS status - WebSocketService: persistent connection with auto-reconnect - ApiConfigService: configurable API base URL - TranslationService: key-value i18n with API fallback Navigation is dynamic (populated from providers), sidebar shows icons-only in collapsed mode with expand on click, dark mode supported throughout. Co-Authored-By: Virgil <virgil@lethean.io>
137 lines
3.7 KiB
TypeScript
137 lines
3.7 KiB
TypeScript
// SPDX-Licence-Identifier: EUPL-1.2
|
|
|
|
import { Component, CUSTOM_ELEMENTS_SCHEMA, Input, OnDestroy, OnInit } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
|
import { TranslationService } from '../services/translation.service';
|
|
import { ProviderDiscoveryService } from '../services/provider-discovery.service';
|
|
import { WebSocketService } from '../services/websocket.service';
|
|
|
|
interface NavItem {
|
|
name: string;
|
|
href: string;
|
|
icon: string;
|
|
}
|
|
|
|
/**
|
|
* ApplicationFrameComponent is the HLCRF (Header, Left nav, Content, Right, Footer)
|
|
* shell for all Core Wails applications. It provides:
|
|
*
|
|
* - Dynamic sidebar navigation populated from ProviderDiscoveryService
|
|
* - Content area rendered via router-outlet for child routes
|
|
* - Footer with time, version, and provider status
|
|
* - Mobile-responsive sidebar with expand/collapse
|
|
* - Dark mode support
|
|
*
|
|
* Ported from core-gui/cmd/lthn-desktop/frontend/src/frame/application.frame.ts
|
|
* with navigation made dynamic via provider discovery.
|
|
*/
|
|
@Component({
|
|
selector: 'application-frame',
|
|
standalone: true,
|
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
|
|
templateUrl: './application-frame.component.html',
|
|
styles: [
|
|
`
|
|
.application-frame {
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.frame-main {
|
|
min-height: calc(100vh - 6.5rem);
|
|
}
|
|
|
|
.connection-dot {
|
|
display: inline-block;
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: rgb(107 114 128);
|
|
margin-right: 0.375rem;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.connection-dot.connected {
|
|
background: rgb(34 197 94);
|
|
box-shadow: 0 0 4px rgb(34 197 94);
|
|
}
|
|
`,
|
|
],
|
|
})
|
|
export class ApplicationFrameComponent implements OnInit, OnDestroy {
|
|
@Input() version = 'v0.1.0';
|
|
|
|
sidebarOpen = false;
|
|
userMenuOpen = false;
|
|
time = '';
|
|
private intervalId: ReturnType<typeof setInterval> | undefined;
|
|
|
|
/** Static navigation items set by the host application. */
|
|
@Input() staticNavigation: NavItem[] = [];
|
|
|
|
/** Combined navigation: static + dynamic from providers. */
|
|
navigation: NavItem[] = [];
|
|
|
|
userNavigation: NavItem[] = [];
|
|
|
|
constructor(
|
|
public t: TranslationService,
|
|
private providerService: ProviderDiscoveryService,
|
|
private wsService: WebSocketService,
|
|
) {}
|
|
|
|
/** Provider count from discovery service. */
|
|
readonly providerCount = () => this.providerService.providers().length;
|
|
|
|
/** WebSocket connection status. */
|
|
readonly wsConnected = () => this.wsService.connected();
|
|
|
|
async ngOnInit(): Promise<void> {
|
|
this.updateTime();
|
|
this.intervalId = setInterval(() => this.updateTime(), 1000);
|
|
|
|
await this.t.onReady();
|
|
this.initUserNavigation();
|
|
|
|
// Discover providers and build navigation
|
|
await this.providerService.discover();
|
|
this.buildNavigation();
|
|
|
|
// Connect WebSocket for real-time updates
|
|
this.wsService.connect();
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
if (this.intervalId) {
|
|
clearInterval(this.intervalId);
|
|
}
|
|
}
|
|
|
|
private initUserNavigation(): void {
|
|
this.userNavigation = [
|
|
{
|
|
name: this.t._('menu.settings'),
|
|
href: 'settings',
|
|
icon: 'fa-regular fa-gear',
|
|
},
|
|
];
|
|
}
|
|
|
|
private buildNavigation(): void {
|
|
const dynamicItems = this.providerService
|
|
.providers()
|
|
.filter((p) => p.element)
|
|
.map((p) => ({
|
|
name: p.name,
|
|
href: p.name.toLowerCase(),
|
|
icon: 'fa-regular fa-puzzle-piece fa-2xl shrink-0',
|
|
}));
|
|
|
|
this.navigation = [...this.staticNavigation, ...dynamicItems];
|
|
}
|
|
|
|
private updateTime(): void {
|
|
this.time = new Date().toLocaleTimeString();
|
|
}
|
|
}
|