import { Component, Inject, OnInit, OnChanges, SimpleChanges, ChangeDetectorRef, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation, effect } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import { BreakpointObserver } from '@angular/cdk/layout'; import { I18nService } from '../../../services/i18n.service'; import { SocketService } from '../../../services/socket.service'; import { CommonService } from '../../../services/common.service'; import { JsonViewDialogService } from '../../../dialogs/json-view-dialog.service'; import { KeyNewOrSetDialogService } from '../../../dialogs/key-new-or-set-dialog.service'; import { MainCommandService } from '../../../services/main-command.service'; import { RedisStateService } from '../../../services/redis-state.service'; import { SettingsService } from '../../../services/settings.service'; import { KeyTypeBase } from './key-type-base'; import { KeyPaging } from './key-paging'; import { KeyPagerInlineComponent } from './key-pager-inline.component'; const intlLocaleMap: Record = { 'zn': 'zh-CN', 'no': 'nb', 'fil': 'tl' }; @Component({ selector: 'p3xr-key-stream', standalone: true, imports: [CommonModule, FormsModule, MatButtonModule, MatIconModule, MatTooltipModule, KeyPagerInlineComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], templateUrl: './key-stream.component.html', encapsulation: ViewEncapsulation.None, }) export class KeyStreamComponent extends KeyTypeBase implements OnInit, OnChanges { paging: KeyPaging; pagedEntries: Array<{ id: string; fields: Array<[string, string]>; data: any; displayData: any; hasDuplicateFields: boolean }> = []; private allEntries: Array<{ id: string; fields: Array<[string, string]>; data: any; displayData: any; hasDuplicateFields: boolean }> = []; constructor( @Inject(I18nService) i18n: I18nService, @Inject(SocketService) socket: SocketService, @Inject(CommonService) common: CommonService, @Inject(JsonViewDialogService) jsonViewDialog: JsonViewDialogService, @Inject(KeyNewOrSetDialogService) keyNewOrSetDialog: KeyNewOrSetDialogService, @Inject(BreakpointObserver) breakpointObserver: BreakpointObserver, @Inject(MainCommandService) cmd: MainCommandService, @Inject(ChangeDetectorRef) cdr: ChangeDetectorRef, @Inject(RedisStateService) redisState: RedisStateService, @Inject(SettingsService) settingsService: SettingsService, ) { super(i18n, socket, common, jsonViewDialog, keyNewOrSetDialog, breakpointObserver, cmd, cdr, redisState, settingsService); this.paging = new KeyPaging({ settingsService }); effect(() => { this.i18n.strings(); if (this.allEntries.length === 0) { return; } this.allEntries = this.allEntries.map((entry) => ({ ...entry, displayData: this.toDisplayData(entry.data, entry.hasDuplicateFields), })); this.updatePagedItems(); this.cdr.markForCheck(); }); } ngOnInit(): void { this.updatePaging(); } ngOnChanges(c: SimpleChanges): void { if (c['p3xrValue']) this.updatePaging(); } updatePaging(): void { if (!this.p3xrValue) return; this.allEntries = this.p3xrValue.map((entry: any) => { const id = entry[0]; const rawData = entry[1]; const fields: Array<[string, string]> = []; for (let i = 0; i < rawData.length; i += 2) { fields.push([rawData[i], rawData[i + 1]]); } const hasDuplicateFields = this.hasDuplicateFields(fields); const data = hasDuplicateFields ? this.fieldsToArray(fields) : this.fieldsToObject(fields); return { id, fields, data, displayData: this.toDisplayData(data, hasDuplicateFields), hasDuplicateFields, }; }); this.paging.figurePaging(this.allEntries.length); this.updatePagedItems(); } updatePagedItems(): void { this.pagedEntries = this.allEntries.slice(this.paging.startIndex, this.paging.endIndex); } showTimestamp(id: string): string { try { const ms = parseInt(id.slice(0, id.indexOf('-'))); const lang = this.i18n.currentLang() || 'en'; const locale = intlLocaleMap[lang] || lang; const date = new Date(ms); return date.toLocaleString(locale, { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } catch { return id; } } isJsonValue(value: string): boolean { if (!value || value.length < 2) return false; const first = value.charAt(0); if (first !== '{' && first !== '[') return false; try { JSON.parse(value); return true; } catch { return false; } } formatJsonValue(value: string): string { try { return JSON.stringify(JSON.parse(value), null, this.settingsService.jsonFormat() ?? 2); } catch { return value; } } private parseFieldValue(value: string): any { try { return JSON.parse(value); } catch { return value; } } private hasDuplicateFields(fields: Array<[string, string]>): boolean { const seen = new Set(); for (const [key] of fields) { if (seen.has(key)) { return true; } seen.add(key); } return false; } private fieldsToObject(fields: Array<[string, string]>): any { const obj: any = {}; for (const [key, value] of fields) { obj[key] = this.parseFieldValue(value); } return obj; } private fieldsToArray(fields: Array<[string, string]>): Array<{ field: string; value: any }> { return fields.map(([field, value]) => ({ field, value: this.parseFieldValue(value), })); } private toDisplayData(data: any, hasDuplicateFields: boolean): any { if (!hasDuplicateFields) { return data; } const fieldLabel = this.strings?.page?.key?.stream?.table?.field || 'Field'; const valueLabel = this.strings?.page?.key?.stream?.table?.value || 'Value'; return data.map((item: { field: string; value: any }) => ({ [fieldLabel]: item.field, [valueLabel]: item.value, })); } private entryToExport(entry: { id: string; fields: Array<[string, string]>; data: any; displayData: any; hasDuplicateFields: boolean }): any { if (entry.hasDuplicateFields) { return { id: entry.id, fields: entry.data, }; } return { id: entry.id, ...entry.data }; } downloadEntry(entry: { id: string; fields: Array<[string, string]> }): void { const lines = [entry.id]; for (const [field, value] of entry.fields) { lines.push(field); lines.push(value); } const text = lines.join('\n'); const blob = new Blob([text], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${this.p3xrKey}-${entry.id}.txt`; a.click(); URL.revokeObjectURL(url); } async copyEntry(entry: { id: string; fields: Array<[string, string]>; data: any; displayData: any; hasDuplicateFields: boolean }): Promise { const obj = this.entryToExport(entry); await this.copy(JSON.stringify(obj, null, 2)); } async viewEntryJson(entry: { id: string; fields: Array<[string, string]>; data: any; displayData: any; hasDuplicateFields: boolean }, event?: Event): Promise { const obj = this.entryToExport(entry); await this.showJson(JSON.stringify(obj), event); } async viewFieldJson(value: string, event?: Event): Promise { await this.showJson(value, event); } copyField(value: any): void { this.copy(value); } async addStream(event: Event): Promise { try { await this.keyNewOrSetDialog.show({ $event: event, type: 'append', model: { type: 'stream', key: this.p3xrKey } }); this.refreshKey(); } catch (e) { this.common.generalHandleError(e); } } async deleteStreamTimestamp(id: string, event: Event): Promise { try { await this.common.confirm({ message: this.i18n.strings().confirm?.deleteStreamTimestamp }); await this.socket.request({ action: 'key-stream-delete-timestamp', payload: { key: this.p3xrKey, streamTimestamp: id } }); this.common.toast(this.i18n.strings().status?.deletedStreamTimestamp || this.i18n.strings().status?.deletedKey || 'Deleted'); this.refreshKey(); } catch (e) { if (e) this.common.generalHandleError(e); } } }