import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs'; import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { ConfigService } from '@services/config.service'; import { PluginCommand, RoleDefinition, PluginBadge, PluginManifest, PluginGroup } from './plugin/plugin.types'; import { PluginStateService } from './plugin/plugin-state.service'; import { PluginBadgeService } from './plugin/plugin-badge.service'; import { PluginRolesService } from './plugin/plugin-roles.service'; // Re-exportar todos los tipos para compatibilidad transparente con componentes existentes export * from './plugin/plugin.types'; @Injectable({ providedIn: 'root' }) export class PluginService { private http = inject(HttpClient); private configService = inject(ConfigService); private translateService = inject(TranslateService); private stateService = inject(PluginStateService); private badgeService = inject(PluginBadgeService); private rolesService = inject(PluginRolesService); private allPlugins: PluginManifest[] = []; private activePlugins: PluginManifest[] = []; private pluginsSubject = new BehaviorSubject([]); public plugins$ = this.pluginsSubject.asObservable(); public statusUpdates$ = this.stateService.statusUpdates$; constructor() { this.refreshPlugins().subscribe(); this.stateService.initMercureSubscription(); // Re-aplicar traducciones dinámicas al cambiar de idioma this.translateService.onLangChange.subscribe((event) => { this.loadPluginTranslations(event.lang); }); } // --- Gestión de estado delegado a PluginStateService --- public setPluginStatus(pluginName: string, idOrUuid: string | number, status: string): void { this.stateService.setPluginStatus(pluginName, idOrUuid, status); } public getPluginStatus(pluginName: string, idOrUuid: string | number): string | undefined { return this.stateService.getPluginStatus(pluginName, idOrUuid); } public setPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number, status: string): void { this.stateService.setPartitionPluginStatus(pluginName, idOrUuid, disk, partition, status); } public getPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): string | undefined { return this.stateService.getPartitionPluginStatus(pluginName, idOrUuid, disk, partition); } public clearPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): void { this.stateService.clearPartitionPluginStatus(pluginName, idOrUuid, disk, partition); } public clearPluginStatus(pluginName: string, idOrUuid: string | number): void { this.stateService.clearPluginStatus(pluginName, idOrUuid); } public setPluginMetadata(pluginName: string, idOrUuid: string | number, metadata: any, disk?: number, partition?: number): void { this.stateService.setPluginMetadata(pluginName, idOrUuid, metadata, disk, partition); } public getPluginMetadata(pluginName: string, idOrUuid: string | number, disk?: number, partition?: number): any { return this.stateService.getPluginMetadata(pluginName, idOrUuid, disk, partition); } public clearPluginMetadata(pluginName: string, idOrUuid: string | number, disk?: number, partition?: number): void { this.stateService.clearPluginMetadata(pluginName, idOrUuid, disk, partition); } public loadActivePluginsStatus(): Observable { return this.stateService.loadActivePluginsStatus(this.activePlugins); } // --- Registro, ciclo de vida e i18n de Plugins --- refreshPlugins(): Observable { const apiUrl = this.configService.apiUrl; if (!apiUrl) { return of([]); } return this.http.get(`${apiUrl}/plugins?all=true`).pipe( catchError(() => of([])), switchMap((plugins) => { if (!plugins || plugins.length === 0) { this.allPlugins = []; this.activePlugins = []; this.pluginsSubject.next([]); return of([]); } // Descubrir y fusionar manifiestos extendidos de cada plugin const manifestRequests = plugins.map(p => this.http.get>(`${apiUrl}/plugins/${p.name}/manifest`).pipe( catchError(() => of({})), map(extra => ({ ...p, ...extra, enabled: Boolean(p.enabled) // La base de datos es la única fuente de verdad para el estado de activación })) ) ); return forkJoin(manifestRequests).pipe( tap((fullPlugins) => { this.allPlugins = fullPlugins; this.activePlugins = fullPlugins.filter(p => p.enabled); this.pluginsSubject.next(this.allPlugins); this.loadPluginTranslations(); this.loadActivePluginsStatus().subscribe(); }) ); }) ); } togglePlugin(name: string, enabled?: boolean): Observable { const apiUrl = this.configService.apiUrl; if (!apiUrl) return of(null); return this.http.post(`${apiUrl}/plugins/${name}/toggle`, enabled !== undefined ? { enabled } : {}).pipe( switchMap(() => this.refreshPlugins()) ); } public loadPluginTranslations(lang?: string): void { const currentLang = lang || this.translateService.currentLang || this.translateService.defaultLang || 'es'; const apiUrl = this.configService.apiUrl; for (const plugin of this.allPlugins) { // 1. Si el plugin declara i18n directamente en el manifest, lo inyectamos if (plugin.i18n && typeof plugin.i18n === 'object') { for (const [l, dict] of Object.entries(plugin.i18n)) { if (dict && typeof dict === 'object') { this.translateService.setTranslation(l, dict, true); } } } // 2. Si el plugin tiene endpoint /i18n/{lang} (carpeta i18n/ estandarizada), lo descargamos bajo demanda if (apiUrl) { this.http.get>(`${apiUrl}/plugins/${plugin.name}/i18n/${currentLang}`).pipe( catchError(() => of(null)), tap((catalog) => { if (catalog && typeof catalog === 'object' && Object.keys(catalog).length > 0) { this.translateService.setTranslation(currentLang, catalog, true); } }) ).subscribe(); } } } getPlugins(): PluginManifest[] { return this.activePlugins.filter(p => p.enabled); } getAllPlugins(): PluginManifest[] { return this.allPlugins; } hasPlugins(): boolean { return this.allPlugins.length > 0; } hasActivePlugins(): boolean { return this.getPlugins().length > 0; } isPluginActive(name: string): boolean { return this.activePlugins.some(p => p.name === name && p.enabled); } getPlugin(name: string): PluginManifest | undefined { return this.allPlugins.find(p => p.name === name); } getPluginGroups(): PluginGroup[] { return this.getPlugins() .filter(p => p.commands && p.commands.length > 0) .map(p => { const translatedTitle = p.titleKey ? this.translateService.instant(p.titleKey) : p.title; const filteredCommands = p.commands .filter(cmd => this.isCommandAllowedForUser(cmd, p.name)) .map(cmd => { const key = cmd.translationKey || cmd.labelKey; if (key) { const trans = this.translateService.instant(key); if (trans && trans !== key) { return { ...cmd, label: trans }; } } return cmd; }); return { plugin: { ...p, title: (translatedTitle && translatedTitle !== p.titleKey) ? translatedTitle : p.title }, commands: filteredCommands }; }) .filter(group => group.commands.length > 0); } getCommands(): PluginCommand[] { const commands: PluginCommand[] = []; for (const group of this.getPluginGroups()) { if (group.commands && Array.isArray(group.commands)) { commands.push(...group.commands); } } return commands; } findCommand(slug: string): PluginCommand | undefined { return this.getCommands().find(c => c.slug === slug); } isCommandSupportedForClients(command: PluginCommand, clients: any[], pluginName?: string): boolean { if (!clients || clients.length === 0) { return true; } if (command.os && command.os.length > 0) { const requiredOsList = command.os.map(os => os.toLowerCase()); const osSupported = clients.every(client => { const statusStr = (client.status || '').toLowerCase(); const matchesStatus = requiredOsList.some(req => statusStr.includes(req)); const hasMatchingPartition = (client.partitions || []).some((partition: any) => { const osName = (partition.operativeSystem?.name || partition.operativeSystem || '').toLowerCase(); return requiredOsList.some(req => osName.includes(req)); }); return matchesStatus || hasMatchingPartition; }); if (!osSupported) { return false; } } if (pluginName && (command.requireStatus || command.excludeStatus)) { return clients.every(client => { const idKey = client.id ? String(client.id) : null; const uuidKey = client.uuid || client.name; const currentStatus = (idKey ? this.getPluginStatus(pluginName, idKey) : null) || (uuidKey ? this.getPluginStatus(pluginName, uuidKey) : null) || 'DISABLED'; if (command.excludeStatus && command.excludeStatus.length > 0) { const excludedUpper = command.excludeStatus.map(s => s.toUpperCase()); if (excludedUpper.includes(currentStatus.toUpperCase())) { return false; } } if (command.requireStatus && command.requireStatus.length > 0) { const requiredUpper = command.requireStatus.map(s => s.toUpperCase()); if (!requiredUpper.includes(currentStatus.toUpperCase())) { return false; } } return true; }); } return true; } // --- Roles y permisos delegados a PluginRolesService --- isCommandAllowedForUser(command: PluginCommand, pluginName: string): boolean { return this.rolesService.isCommandAllowedForUser(command, this.getPluginConfig(pluginName)); } getUserRoles(): Observable { return this.rolesService.getUserRoles(); } getCommandRoles(pluginName: string, commandSlug: string): string[] { return this.rolesService.getCommandRoles(this.getPlugin(pluginName), commandSlug, this.getPluginConfig(pluginName)); } savePluginCommandRoles(pluginName: string, commandRoles: Record): void { const currentConfig = this.getPluginConfig(pluginName); const updatedConfig = { ...currentConfig, command_roles: commandRoles }; this.savePluginConfig(pluginName, updatedConfig); } // --- Badges delegados a PluginBadgeService --- getClientBadges(client: any): PluginBadge[] { return this.badgeService.getClientBadges(this.activePlugins, client); } getPartitionBadges(client: any, diskNumber: number, partitionNumber: number): PluginBadge[] { return this.badgeService.getPartitionBadges(this.activePlugins, client, diskNumber, partitionNumber); } // --- Configuración de Plugins (Local + Backend) --- getPluginConfig(pluginName: string): Record { const plugin = this.getPlugin(pluginName); const defaults = plugin?.defaultConfig || {}; const storageKey = `og_plugin_config_${pluginName}`; const saved = localStorage.getItem(storageKey); if (saved) { try { return { ...defaults, ...(plugin?.config || {}), ...JSON.parse(saved) }; } catch { return { ...defaults, ...(plugin?.config || {}) }; } } return { ...defaults, ...(plugin?.config || {}) }; } savePluginConfig(pluginName: string, config: Record): void { const storageKey = `og_plugin_config_${pluginName}`; localStorage.setItem(storageKey, JSON.stringify(config)); const plugin = this.getPlugin(pluginName); if (plugin) { plugin.config = config; } const apiUrl = this.configService.apiUrl; if (apiUrl) { this.http.post(`${apiUrl}/plugins/${pluginName}/config`, config).pipe( catchError((err) => { console.warn(`[PluginService] Could not persist config for ${pluginName} to backend`, err); return of(null); }) ).subscribe(); } } resetPluginConfig(pluginName: string): Record { const plugin = this.getPlugin(pluginName); const defaults = plugin?.defaultConfig || {}; const storageKey = `og_plugin_config_${pluginName}`; localStorage.removeItem(storageKey); this.savePluginConfig(pluginName, defaults); return defaults; } }