feat(plugins): add syntax highlighting for script viewer and fix blank exclusions handling
parent
67572c313c
commit
a5c4b7b43a
|
|
@ -10,10 +10,12 @@
|
|||
- Generador dinámico de formularios de configuración basados en esquemas JSON (`app-plugin-settings`) con botón de autogeneración y actualización reactiva de scripts según los parámetros seleccionados.
|
||||
- Soporte de insignias de estado (badges) contextuales por cliente en las vistas de mosaico y lista de equipos (`groups.component`).
|
||||
- Nuevo elemento condicional "Plugins" en el menú lateral (`sidebar`), visible únicamente cuando existen plugins activos en el sistema.
|
||||
- Resaltador de sintaxis enriquecido y visor de terminal para scripts de configuración con numeración de líneas, colores semánticos (comentarios en verde itálica, comandos, parámetros, cadenas), botón de copiado rápido y alternancia entre vista coloreada y editor manual.
|
||||
- Comportamiento 100% neutro e idéntico a 1.15.0 si no hay plugins instalados.
|
||||
|
||||
### Fixed
|
||||
- **Recarga de página en Plugins (`plugins.component`, `plugin-settings.component`)**: Corregido el problema de sincronización reactiva al refrescar directamente (F5) la vista de `/plugins`, añadiendo un estado de carga visual y suscripción a `plugins$` para evitar mostrar prematuramente la pantalla de "No hay plugins instalados".
|
||||
- **Rutas excluidas vacías en ogfreeze (`plugin-settings.component`)**: Corregida la regeneración de scripts cuando el campo de rutas excluidas se deja intencionadamente en blanco; ahora omite de forma limpia el bloque de exclusiones en lugar de forzar los valores predeterminados.
|
||||
|
||||
## [1.15.0] - 2026-10-20
|
||||
|
||||
|
|
|
|||
|
|
@ -181,46 +181,203 @@
|
|||
height: 48px;
|
||||
}
|
||||
|
||||
/* Visor de código para scripts con alto contraste */
|
||||
.code-field {
|
||||
/* Editor de código y visor con resaltado de sintaxis */
|
||||
.code-editor-box {
|
||||
margin-top: 4px;
|
||||
background-color: #0b1120;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.code-field .mat-mdc-text-field-wrapper {
|
||||
background-color: #0b1120 !important;
|
||||
border-radius: 8px !important;
|
||||
.code-editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #0f172a;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
}
|
||||
|
||||
textarea.code-textarea,
|
||||
.code-field textarea.mat-mdc-input-element {
|
||||
font-family: 'Consolas', 'Fira Code', 'Monaco', monospace !important;
|
||||
font-size: 0.92rem !important;
|
||||
line-height: 1.6 !important;
|
||||
background-color: #0b1120 !important;
|
||||
color: #38bdf8 !important;
|
||||
-webkit-text-fill-color: #38bdf8 !important;
|
||||
caret-color: #38bdf8 !important;
|
||||
padding: 14px !important;
|
||||
border-radius: 6px !important;
|
||||
.code-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.code-field .mdc-notched-outline__leading,
|
||||
.code-field .mdc-notched-outline__notch,
|
||||
.code-field .mdc-notched-outline__trailing {
|
||||
border-color: #334155 !important;
|
||||
.terminal-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.code-field:hover .mdc-notched-outline__leading,
|
||||
.code-field:hover .mdc-notched-outline__notch,
|
||||
.code-field:hover .mdc-notched-outline__trailing {
|
||||
border-color: #64748b !important;
|
||||
.terminal-dot.red {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
.code-field.mat-focused .mdc-notched-outline__leading,
|
||||
.code-field.mat-focused .mdc-notched-outline__notch,
|
||||
.code-field.mat-focused .mdc-notched-outline__trailing {
|
||||
border-color: #38bdf8 !important;
|
||||
border-width: 2px !important;
|
||||
.terminal-dot.yellow {
|
||||
background-color: #f59e0b;
|
||||
}
|
||||
|
||||
.terminal-dot.green {
|
||||
background-color: #10b981;
|
||||
}
|
||||
|
||||
.code-lang-badge {
|
||||
font-family: 'Consolas', 'Fira Code', 'Monaco', monospace;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
background: #1e293b;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.code-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.code-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.code-action-btn:hover {
|
||||
background: #334155;
|
||||
color: #f8fafc;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.code-action-btn.active {
|
||||
background: #0284c7;
|
||||
color: #ffffff;
|
||||
border-color: #0284c7;
|
||||
}
|
||||
|
||||
.action-btn-icon {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.code-preview-scroll {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
padding: 10px 0;
|
||||
background: #0b1120;
|
||||
}
|
||||
|
||||
.code-lines-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
font-family: 'Consolas', 'Fira Code', 'Monaco', monospace;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.code-line-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.code-line-row:hover {
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.line-number {
|
||||
min-width: 42px;
|
||||
padding: 0 12px;
|
||||
text-align: right;
|
||||
color: #475569;
|
||||
user-select: none;
|
||||
border-right: 1px solid #1e293b;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.line-code {
|
||||
padding: 0 16px;
|
||||
white-space: pre;
|
||||
color: #f8fafc;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Tokens de resaltado de sintaxis */
|
||||
.syntax-comment {
|
||||
color: #86efac;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.syntax-cmd {
|
||||
color: #38bdf8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.syntax-keyword {
|
||||
color: #c084fc;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.syntax-flag {
|
||||
color: #f472b6;
|
||||
}
|
||||
|
||||
.syntax-string {
|
||||
color: #fde047;
|
||||
}
|
||||
|
||||
.syntax-val {
|
||||
color: #34d399;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.syntax-num {
|
||||
color: #fb923c;
|
||||
}
|
||||
|
||||
.code-textarea-wrapper {
|
||||
padding: 10px;
|
||||
background: #0b1120;
|
||||
}
|
||||
|
||||
.code-raw-textarea {
|
||||
width: 100%;
|
||||
background-color: #090d16;
|
||||
color: #f8fafc;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-family: 'Consolas', 'Fira Code', 'Monaco', monospace;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.6;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.code-raw-textarea:focus {
|
||||
border-color: #38bdf8;
|
||||
box-shadow: 0 0 0 1px #38bdf8;
|
||||
}
|
||||
|
||||
.form-actions-bar {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
@switch (field.type) {
|
||||
@case ('select') {
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-select [id]="'field-' + field.key" [(ngModel)]="configData[field.key]">
|
||||
<mat-select [id]="'field-' + field.key" [(ngModel)]="configData[field.key]" (ngModelChange)="onParamChange()">
|
||||
@for (opt of field.options; track opt) {
|
||||
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
|
||||
@case ('checkbox') {
|
||||
<div class="toggle-wrapper">
|
||||
<mat-slide-toggle [id]="'field-' + field.key" color="primary" [(ngModel)]="configData[field.key]">
|
||||
<mat-slide-toggle [id]="'field-' + field.key" color="primary" [(ngModel)]="configData[field.key]" (ngModelChange)="onParamChange()">
|
||||
{{ configData[field.key] ? 'Habilitado' : 'Deshabilitado' }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
|
@ -76,20 +76,61 @@
|
|||
|
||||
@case ('number') {
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<input matInput type="number" [id]="'field-' + field.key" [(ngModel)]="configData[field.key]">
|
||||
<input matInput type="number" [id]="'field-' + field.key" [(ngModel)]="configData[field.key]" (ngModelChange)="onParamChange()">
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@case ('text') {
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<input matInput type="text" [id]="'field-' + field.key" [(ngModel)]="configData[field.key]">
|
||||
<input matInput type="text" [id]="'field-' + field.key" [(ngModel)]="configData[field.key]" (ngModelChange)="onParamChange()">
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@case ('textarea') {
|
||||
<mat-form-field appearance="outline" class="w-100 code-field">
|
||||
<textarea matInput rows="7" [id]="'field-' + field.key" class="code-textarea" [(ngModel)]="configData[field.key]"></textarea>
|
||||
</mat-form-field>
|
||||
<div class="code-editor-box">
|
||||
<div class="code-editor-header">
|
||||
<div class="code-header-left">
|
||||
<span class="terminal-dot red"></span>
|
||||
<span class="terminal-dot yellow"></span>
|
||||
<span class="terminal-dot green"></span>
|
||||
<span class="code-lang-badge">PowerShell</span>
|
||||
</div>
|
||||
<div class="code-header-actions">
|
||||
<button type="button" class="code-action-btn" (click)="copyToClipboard(configData[field.key])" title="Copiar script">
|
||||
<mat-icon class="action-btn-icon">content_copy</mat-icon>
|
||||
<span>Copiar</span>
|
||||
</button>
|
||||
<button type="button" class="code-action-btn" [class.active]="isEditingScript(field.key)" (click)="toggleScriptEdit(field.key)">
|
||||
<mat-icon class="action-btn-icon">{{ isEditingScript(field.key) ? 'visibility' : 'edit' }}</mat-icon>
|
||||
<span>{{ isEditingScript(field.key) ? 'Ver coloreado' : 'Editar código' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!isEditingScript(field.key)) {
|
||||
<div class="code-preview-scroll">
|
||||
<div class="code-lines-table">
|
||||
@for (line of getScriptLines(configData[field.key]); track $index) {
|
||||
<div class="code-line-row">
|
||||
<span class="line-number">{{ $index + 1 }}</span>
|
||||
<span class="line-code" [innerHTML]="line"></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="code-textarea-wrapper">
|
||||
<textarea
|
||||
[id]="'field-' + field.key"
|
||||
class="code-raw-textarea"
|
||||
rows="9"
|
||||
[(ngModel)]="configData[field.key]"
|
||||
(ngModelChange)="onManualScriptInput(field.key)"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
|
||||
import { PluginManifest, PluginService, SchemaField } from '@services/plugin.service';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
|
@ -14,7 +15,11 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
plugin?: PluginManifest;
|
||||
schema: SchemaField[] = [];
|
||||
configData: Record<string, any> = {};
|
||||
editingScripts: Record<string, boolean> = {};
|
||||
hasManualScriptEdits: boolean = false;
|
||||
|
||||
private pluginSub?: Subscription;
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
|
||||
constructor(
|
||||
private pluginService: PluginService,
|
||||
|
|
@ -51,41 +56,147 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
return this.schema.some(f => f.key === 'freeze_script' || f.key === 'unfreeze_script');
|
||||
}
|
||||
|
||||
regenerateScripts(): void {
|
||||
onParamChange(): void {
|
||||
if (!this.hasManualScriptEdits) {
|
||||
this.regenerateScripts(false);
|
||||
}
|
||||
}
|
||||
|
||||
onManualScriptInput(key: string): void {
|
||||
this.hasManualScriptEdits = true;
|
||||
}
|
||||
|
||||
isEditingScript(key: string): boolean {
|
||||
return !!this.editingScripts[key];
|
||||
}
|
||||
|
||||
toggleScriptEdit(key: string): void {
|
||||
this.editingScripts[key] = !this.editingScripts[key];
|
||||
}
|
||||
|
||||
copyToClipboard(text: string): void {
|
||||
if (!text) return;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
this.toast.success('Script copiado al portapapeles', 'Portapapeles');
|
||||
}).catch(() => {
|
||||
this.toast.error('No se pudo copiar el script al portapapeles', 'Error');
|
||||
});
|
||||
}
|
||||
|
||||
getScriptLines(script: string): SafeHtml[] {
|
||||
if (!script) return [];
|
||||
const lines = script.split('\n');
|
||||
return lines.map(line => this.highlightLine(line));
|
||||
}
|
||||
|
||||
private escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
private highlightLine(rawLine: string): SafeHtml {
|
||||
const trimmed = rawLine.trim();
|
||||
|
||||
if (trimmed.length === 0) {
|
||||
return this.sanitizer.bypassSecurityTrustHtml(' ');
|
||||
}
|
||||
|
||||
// Comentario completo
|
||||
if (trimmed.startsWith('#')) {
|
||||
const escaped = this.escapeHtml(rawLine);
|
||||
return this.sanitizer.bypassSecurityTrustHtml(`<span class="syntax-comment">${escaped}</span>`);
|
||||
}
|
||||
|
||||
let escaped = this.escapeHtml(rawLine);
|
||||
|
||||
// 1. Cadenas entre comillas dobles o simples
|
||||
const strings: string[] = [];
|
||||
escaped = escaped.replace(/(["'])(.*?)\1/g, (match) => {
|
||||
strings.push(match);
|
||||
return `___STR_${strings.length - 1}___`;
|
||||
});
|
||||
|
||||
// 2. Parámetros / flags (/r, /t, -c, etc.)
|
||||
escaped = escaped.replace(/(\s)([\/-][a-zA-Z0-9?]+)/g, '$1<span class="syntax-flag">$2</span>');
|
||||
|
||||
// 3. Comandos ejecutables
|
||||
escaped = escaped.replace(/\b(uwfmgr\.exe|shutdown|powershell\.exe|cmd\.exe)\b/gi, '<span class="syntax-cmd">$1</span>');
|
||||
|
||||
// 4. Subcomandos y palabras clave
|
||||
escaped = escaped.replace(/\b(overlay|filter|volume|file|enable|disable|protect|set-type|set-size|add-exclusion)\b/gi, '<span class="syntax-keyword">$1</span>');
|
||||
|
||||
// 5. Valores y tipos
|
||||
escaped = escaped.replace(/\b(RAM|DISK|true|false)\b/gi, '<span class="syntax-val">$1</span>');
|
||||
|
||||
// 6. Números
|
||||
escaped = escaped.replace(/\b(\d+)\b/g, '<span class="syntax-num">$1</span>');
|
||||
|
||||
// Restaurar cadenas
|
||||
escaped = escaped.replace(/___STR_(\d+)___/g, (_, index) => {
|
||||
const original = strings[Number(index)];
|
||||
return `<span class="syntax-string">${original}</span>`;
|
||||
});
|
||||
|
||||
return this.sanitizer.bypassSecurityTrustHtml(escaped);
|
||||
}
|
||||
|
||||
regenerateScripts(showToast: boolean = true): void {
|
||||
if (!this.pluginName) return;
|
||||
|
||||
if (this.pluginName === 'ogfreeze') {
|
||||
const overlayType = this.configData['overlay_type'] || 'RAM';
|
||||
const overlaySize = this.configData['overlay_size'] || 2048;
|
||||
const protectC = this.configData['protect_c'] !== false;
|
||||
const exclusionsRaw = this.configData['exclusions'] || 'C:\\.ogimage, C:\\OpenGnsys';
|
||||
const exclusions = exclusionsRaw
|
||||
.split(',')
|
||||
.map((s: string) => s.trim())
|
||||
.filter((s: string) => s.length > 0);
|
||||
|
||||
let freezeScript = `# 1. Excluir metadatos y carpetas del filtro UWF\n`;
|
||||
for (const excl of exclusions) {
|
||||
freezeScript += `uwfmgr.exe file add-exclusion "${excl}"\n`;
|
||||
// Obtener exclusiones de forma estricta (no usar fallback si el usuario las borró)
|
||||
const rawExcl = this.configData['exclusions'];
|
||||
const exclusionsRaw = (rawExcl !== undefined && rawExcl !== null) ? String(rawExcl).trim() : '';
|
||||
const exclusions = exclusionsRaw.length > 0
|
||||
? exclusionsRaw.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0)
|
||||
: [];
|
||||
|
||||
let freezeScript = '';
|
||||
let step = 1;
|
||||
|
||||
// Solo incluir el paso de exclusiones si el usuario ha especificado rutas
|
||||
if (exclusions.length > 0) {
|
||||
freezeScript += `# ${step}. Excluir metadatos y carpetas del filtro UWF\n`;
|
||||
for (const excl of exclusions) {
|
||||
freezeScript += `uwfmgr.exe file add-exclusion "${excl}"\n`;
|
||||
}
|
||||
freezeScript += `\n`;
|
||||
step++;
|
||||
}
|
||||
freezeScript += `\n# 2. Configurar buffer temporal de escritura\n`;
|
||||
|
||||
freezeScript += `# ${step}. Configurar buffer temporal de escritura\n`;
|
||||
freezeScript += `uwfmgr.exe overlay set-type ${overlayType}\n`;
|
||||
freezeScript += `uwfmgr.exe overlay set-size ${overlaySize}\n\n`;
|
||||
step++;
|
||||
|
||||
freezeScript += `# 3. Proteger volumen y habilitar filtro\n`;
|
||||
freezeScript += `# ${step}. Proteger volumen y habilitar filtro\n`;
|
||||
if (protectC) {
|
||||
freezeScript += `uwfmgr.exe volume protect C:\n`;
|
||||
}
|
||||
freezeScript += `uwfmgr.exe filter enable\n\n`;
|
||||
freezeScript += `# 4. Reinicio obligatorio para que entre en vigor\n`;
|
||||
step++;
|
||||
|
||||
freezeScript += `# ${step}. Reinicio obligatorio para que entre en vigor\n`;
|
||||
freezeScript += `shutdown /r /t 5 -c "OpenGnsys: Reiniciando para aplicar congelacion de sistema"`;
|
||||
|
||||
this.configData['freeze_script'] = freezeScript;
|
||||
this.configData['unfreeze_script'] = `# Desactivar filtro de protección UWF en el próximo inicio\nuwfmgr.exe filter disable\n\n# Reiniciar para aplicar cambios\nshutdown /r /t 5 -c "OpenGnsys: Reiniciando para descongelar sistema"`;
|
||||
|
||||
this.toast.info('Scripts de PowerShell generados a partir de los parámetros actuales', this.plugin?.title || 'Plugin');
|
||||
this.hasManualScriptEdits = false;
|
||||
|
||||
if (showToast) {
|
||||
this.toast.info('Scripts de PowerShell generados a partir de los parámetros actuales', this.plugin?.title || 'Plugin');
|
||||
}
|
||||
} else {
|
||||
this.toast.info('Scripts actualizados con los parámetros predeterminados', this.plugin?.title || 'Plugin');
|
||||
if (showToast) {
|
||||
this.toast.info('Scripts actualizados con los parámetros predeterminados', this.plugin?.title || 'Plugin');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +209,7 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
reset(): void {
|
||||
if (!this.pluginName) return;
|
||||
this.configData = this.pluginService.resetPluginConfig(this.pluginName);
|
||||
this.hasManualScriptEdits = false;
|
||||
this.toast.info('Configuración restaurada a valores por defecto', this.plugin?.title || 'Plugin');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue