perf: Add ram metric
This commit is contained in:
23
electron/app-metrics.ts
Normal file
23
electron/app-metrics.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
|
|
||||||
|
export interface AppMetricsProcessSnapshot {
|
||||||
|
pid: number;
|
||||||
|
type: string;
|
||||||
|
workingSetKb: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppMetricsSnapshot {
|
||||||
|
collectedAt: number;
|
||||||
|
processes: AppMetricsProcessSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectAppMetricsSnapshot(): AppMetricsSnapshot {
|
||||||
|
return {
|
||||||
|
collectedAt: Date.now(),
|
||||||
|
processes: app.getAppMetrics().map((metric) => ({
|
||||||
|
pid: metric.pid,
|
||||||
|
type: metric.type,
|
||||||
|
workingSetKb: metric.memory?.workingSetSize ?? null
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -60,6 +60,7 @@ import {
|
|||||||
} from '../data-management';
|
} from '../data-management';
|
||||||
import { listRunningProcessNames } from '../process-list';
|
import { listRunningProcessNames } from '../process-list';
|
||||||
import { detectActiveGame } from '../game-detection';
|
import { detectActiveGame } from '../game-detection';
|
||||||
|
import { collectAppMetricsSnapshot } from '../app-metrics';
|
||||||
|
|
||||||
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
||||||
const MAX_ACTIVE_DESKTOP_NOTIFICATIONS = 20;
|
const MAX_ACTIVE_DESKTOP_NOTIFICATIONS = 20;
|
||||||
@@ -362,6 +363,8 @@ export function setupSystemHandlers(): void {
|
|||||||
return await stopLinuxScreenShareMonitorCapture(captureId);
|
return await stopLinuxScreenShareMonitorCapture(captureId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-app-metrics', () => collectAppMetricsSnapshot());
|
||||||
|
|
||||||
ipcMain.handle('get-app-data-path', () => app.getPath('userData'));
|
ipcMain.handle('get-app-data-path', () => app.getPath('userData'));
|
||||||
ipcMain.handle('open-current-data-folder', async () => await openCurrentDataFolder());
|
ipcMain.handle('open-current-data-folder', async () => await openCurrentDataFolder());
|
||||||
ipcMain.handle('export-user-data', async () => await exportUserData());
|
ipcMain.handle('export-user-data', async () => await exportUserData());
|
||||||
|
|||||||
@@ -240,6 +240,14 @@ export interface ElectronAPI {
|
|||||||
stopLinuxScreenShareMonitorCapture: (captureId?: string) => Promise<boolean>;
|
stopLinuxScreenShareMonitorCapture: (captureId?: string) => Promise<boolean>;
|
||||||
onLinuxScreenShareMonitorAudioChunk: (listener: (payload: LinuxScreenShareMonitorAudioChunkPayload) => void) => () => void;
|
onLinuxScreenShareMonitorAudioChunk: (listener: (payload: LinuxScreenShareMonitorAudioChunkPayload) => void) => () => void;
|
||||||
onLinuxScreenShareMonitorAudioEnded: (listener: (payload: LinuxScreenShareMonitorAudioEndedPayload) => void) => () => void;
|
onLinuxScreenShareMonitorAudioEnded: (listener: (payload: LinuxScreenShareMonitorAudioEndedPayload) => void) => () => void;
|
||||||
|
getAppMetrics: () => Promise<{
|
||||||
|
collectedAt: number;
|
||||||
|
processes: {
|
||||||
|
pid: number;
|
||||||
|
type: string;
|
||||||
|
workingSetKb: number | null;
|
||||||
|
}[];
|
||||||
|
}>;
|
||||||
getAppDataPath: () => Promise<string>;
|
getAppDataPath: () => Promise<string>;
|
||||||
openCurrentDataFolder: () => Promise<boolean>;
|
openCurrentDataFolder: () => Promise<boolean>;
|
||||||
exportUserData: () => Promise<ExportUserDataResult>;
|
exportUserData: () => Promise<ExportUserDataResult>;
|
||||||
@@ -374,6 +382,7 @@ const electronAPI: ElectronAPI = {
|
|||||||
ipcRenderer.removeListener(LINUX_SCREEN_SHARE_MONITOR_AUDIO_ENDED_CHANNEL, wrappedListener);
|
ipcRenderer.removeListener(LINUX_SCREEN_SHARE_MONITOR_AUDIO_ENDED_CHANNEL, wrappedListener);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getAppMetrics: () => ipcRenderer.invoke('get-app-metrics'),
|
||||||
getAppDataPath: () => ipcRenderer.invoke('get-app-data-path'),
|
getAppDataPath: () => ipcRenderer.invoke('get-app-data-path'),
|
||||||
openCurrentDataFolder: () => ipcRenderer.invoke('open-current-data-folder'),
|
openCurrentDataFolder: () => ipcRenderer.invoke('open-current-data-folder'),
|
||||||
exportUserData: () => ipcRenderer.invoke('export-user-data'),
|
exportUserData: () => ipcRenderer.invoke('export-user-data'),
|
||||||
|
|||||||
@@ -233,6 +233,17 @@ export interface ActiveGameCandidateResult {
|
|||||||
fallbackProcessNames: string[];
|
fallbackProcessNames: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ElectronAppMetricsProcess {
|
||||||
|
pid: number;
|
||||||
|
type: string;
|
||||||
|
workingSetKb: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ElectronAppMetricsSnapshot {
|
||||||
|
collectedAt: number;
|
||||||
|
processes: ElectronAppMetricsProcess[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ElectronApi {
|
export interface ElectronApi {
|
||||||
linuxDisplayServer: string;
|
linuxDisplayServer: string;
|
||||||
minimizeWindow: () => void;
|
minimizeWindow: () => void;
|
||||||
@@ -251,6 +262,7 @@ export interface ElectronApi {
|
|||||||
stopLinuxScreenShareMonitorCapture: (captureId?: string) => Promise<boolean>;
|
stopLinuxScreenShareMonitorCapture: (captureId?: string) => Promise<boolean>;
|
||||||
onLinuxScreenShareMonitorAudioChunk: (listener: (payload: LinuxScreenShareMonitorAudioChunkPayload) => void) => () => void;
|
onLinuxScreenShareMonitorAudioChunk: (listener: (payload: LinuxScreenShareMonitorAudioChunkPayload) => void) => () => void;
|
||||||
onLinuxScreenShareMonitorAudioEnded: (listener: (payload: LinuxScreenShareMonitorAudioEndedPayload) => void) => () => void;
|
onLinuxScreenShareMonitorAudioEnded: (listener: (payload: LinuxScreenShareMonitorAudioEndedPayload) => void) => () => void;
|
||||||
|
getAppMetrics: () => Promise<ElectronAppMetricsSnapshot>;
|
||||||
getAppDataPath: () => Promise<string>;
|
getAppDataPath: () => Promise<string>;
|
||||||
openCurrentDataFolder: () => Promise<boolean>;
|
openCurrentDataFolder: () => Promise<boolean>;
|
||||||
exportUserData: () => Promise<ExportUserDataResult>;
|
exportUserData: () => Promise<ExportUserDataResult>;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it
|
||||||
|
} from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatAppRamLabel,
|
||||||
|
formatKilobytesAsMegabytes,
|
||||||
|
sumWorkingSetKb
|
||||||
|
} from './electron-app-metrics.rules';
|
||||||
|
import type { ElectronAppMetricsSnapshot } from './electron-app-metrics.rules';
|
||||||
|
|
||||||
|
function createSnapshot(
|
||||||
|
processes: ElectronAppMetricsSnapshot['processes']
|
||||||
|
): ElectronAppMetricsSnapshot {
|
||||||
|
return {
|
||||||
|
collectedAt: 1,
|
||||||
|
processes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('sumWorkingSetKb', () => {
|
||||||
|
it('sums working set across processes that report memory', () => {
|
||||||
|
const total = sumWorkingSetKb([{ pid: 1, type: 'Browser', workingSetKb: 1024 }, { pid: 2, type: 'GPU', workingSetKb: 512 }]);
|
||||||
|
|
||||||
|
expect(total).toBe(1536);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores processes without memory readings', () => {
|
||||||
|
const total = sumWorkingSetKb([{ pid: 1, type: 'Browser', workingSetKb: 2048 }, { pid: 2, type: 'Unknown', workingSetKb: null }]);
|
||||||
|
|
||||||
|
expect(total).toBe(2048);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when no process reports memory', () => {
|
||||||
|
expect(sumWorkingSetKb([{ pid: 1, type: 'Browser', workingSetKb: null }])).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatKilobytesAsMegabytes', () => {
|
||||||
|
it('rounds large values to whole megabytes', () => {
|
||||||
|
expect(formatKilobytesAsMegabytes(412 * 1024)).toBe('412 MB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps one decimal for medium values', () => {
|
||||||
|
expect(formatKilobytesAsMegabytes(15.4 * 1024)).toBe('15.4 MB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps two decimals for small values', () => {
|
||||||
|
expect(formatKilobytesAsMegabytes(1.25 * 1024)).toBe('1.25 MB');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAppRamLabel', () => {
|
||||||
|
it('formats total working set as a compact RAM label', () => {
|
||||||
|
const browser = { pid: 1, type: 'Browser', workingSetKb: 300 * 1024 };
|
||||||
|
const gpu = { pid: 2, type: 'GPU', workingSetKb: 112 * 1024 };
|
||||||
|
const label = formatAppRamLabel(createSnapshot([browser, gpu]));
|
||||||
|
|
||||||
|
expect(label).toBe('412 MB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when metrics contain no memory readings', () => {
|
||||||
|
expect(formatAppRamLabel(createSnapshot([{ pid: 1, type: 'Browser', workingSetKb: null }]))).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export interface ElectronAppMetricsProcess {
|
||||||
|
pid: number;
|
||||||
|
type: string;
|
||||||
|
workingSetKb: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ElectronAppMetricsSnapshot {
|
||||||
|
collectedAt: number;
|
||||||
|
processes: ElectronAppMetricsProcess[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sumWorkingSetKb(processes: ElectronAppMetricsProcess[]): number | null {
|
||||||
|
let total = 0;
|
||||||
|
let hasAny = false;
|
||||||
|
|
||||||
|
for (const process of processes) {
|
||||||
|
if (process.workingSetKb == null || process.workingSetKb < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
total += process.workingSetKb;
|
||||||
|
hasAny = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasAny ? total : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatKilobytesAsMegabytes(kilobytes: number): string {
|
||||||
|
const megabytes = kilobytes / 1024;
|
||||||
|
|
||||||
|
if (megabytes >= 100)
|
||||||
|
return `${Math.round(megabytes)} MB`;
|
||||||
|
|
||||||
|
if (megabytes >= 10)
|
||||||
|
return `${megabytes.toFixed(1)} MB`;
|
||||||
|
|
||||||
|
return `${megabytes.toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAppRamLabel(snapshot: ElectronAppMetricsSnapshot): string | null {
|
||||||
|
const totalKb = sumWorkingSetKb(snapshot.processes);
|
||||||
|
|
||||||
|
if (totalKb == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return formatKilobytesAsMegabytes(totalKb);
|
||||||
|
}
|
||||||
@@ -335,9 +335,7 @@
|
|||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
@if (dragActive()) {
|
@if (dragActive()) {
|
||||||
<div
|
<div class="pointer-events-none absolute inset-0 flex items-center justify-center border-2 border-dashed border-primary bg-primary/5">
|
||||||
class="pointer-events-none absolute inset-0 flex items-center justify-center border-2 border-dashed border-primary bg-primary/5"
|
|
||||||
>
|
|
||||||
<div class="text-sm text-muted-foreground">Drop files to attach</div>
|
<div class="text-sm text-muted-foreground">Drop files to attach</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,9 @@
|
|||||||
name="lucideImage"
|
name="lucideImage"
|
||||||
class="h-5 w-5 text-primary"
|
class="h-5 w-5 text-primary"
|
||||||
/>
|
/>
|
||||||
<span class="chat-image-grid-loading-label">{{ ((gridImage.receivedBytes || 0) * 100) / gridImage.size | number: '1.0-0' }}%</span>
|
<span class="chat-image-grid-loading-label"
|
||||||
|
>{{ ((gridImage.receivedBytes || 0) * 100) / gridImage.size | number: '1.0-0' }}%</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="chat-image-grid-cell chat-image-grid-loading">
|
<div class="chat-image-grid-cell chat-image-grid-loading">
|
||||||
@@ -231,216 +233,249 @@
|
|||||||
}
|
}
|
||||||
@for (att of attachmentsList; track att.id) {
|
@for (att of attachmentsList; track att.id) {
|
||||||
@if (shouldShowAttachmentInList(att)) {
|
@if (shouldShowAttachmentInList(att)) {
|
||||||
@if (isImageLikeAttachment(att) && !imageGridLayout().useGrid) {
|
@if (isImageLikeAttachment(att) && !imageGridLayout().useGrid) {
|
||||||
@if (att.available && att.objectUrl) {
|
@if (att.available && att.objectUrl) {
|
||||||
<div
|
<div
|
||||||
class="group/img relative inline-block"
|
class="group/img relative inline-block"
|
||||||
(contextmenu)="openImageContextMenu($event, att)"
|
(contextmenu)="openImageContextMenu($event, att)"
|
||||||
>
|
|
||||||
<img
|
|
||||||
[src]="att.objectUrl"
|
|
||||||
[alt]="att.filename"
|
|
||||||
class="max-h-80 w-auto cursor-pointer rounded-md"
|
|
||||||
(click)="openLightbox(att)"
|
|
||||||
/>
|
|
||||||
<div class="pointer-events-none absolute inset-0 rounded-md bg-black/0 transition-colors group-hover/img:bg-black/20"></div>
|
|
||||||
<div class="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover/img:opacity-100">
|
|
||||||
<button
|
|
||||||
(click)="openLightbox(att); $event.stopPropagation()"
|
|
||||||
class="grid h-7 w-7 place-items-center rounded-md bg-black/60 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
|
||||||
title="View full size"
|
|
||||||
>
|
|
||||||
<ng-icon
|
|
||||||
name="lucideExpand"
|
|
||||||
class="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
(click)="downloadAttachment(att); $event.stopPropagation()"
|
|
||||||
class="grid h-7 w-7 place-items-center rounded-md bg-black/60 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
|
||||||
title="Download"
|
|
||||||
>
|
|
||||||
<ng-icon
|
|
||||||
name="lucideDownload"
|
|
||||||
class="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} @else if ((att.receivedBytes || 0) > 0) {
|
|
||||||
<div
|
|
||||||
appThemeNode="chatAttachmentCard"
|
|
||||||
class="max-w-xs rounded-md border border-border bg-secondary/40 p-3"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md bg-primary/10">
|
|
||||||
<ng-icon
|
|
||||||
name="lucideImage"
|
|
||||||
class="h-5 w-5 text-primary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<div class="truncate text-sm font-medium">{{ att.filename }}</div>
|
|
||||||
<div class="text-xs text-muted-foreground">{{ formatBytes(att.receivedBytes || 0) }} / {{ formatBytes(att.size) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs font-medium text-primary">{{ ((att.receivedBytes || 0) * 100) / att.size | number: '1.0-0' }}%</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full bg-primary transition-all duration-300"
|
|
||||||
[style.width.%]="((att.receivedBytes || 0) * 100) / att.size"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
<div
|
|
||||||
appThemeNode="chatAttachmentCard"
|
|
||||||
class="max-w-xs rounded-md border border-dashed border-border bg-secondary/20 p-4"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md bg-muted">
|
|
||||||
<ng-icon
|
|
||||||
name="lucideImage"
|
|
||||||
class="h-5 w-5 text-muted-foreground"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<div class="truncate text-sm font-medium text-foreground">{{ att.filename }}</div>
|
|
||||||
<div class="text-xs text-muted-foreground">{{ formatBytes(att.size) }}</div>
|
|
||||||
<div
|
|
||||||
class="mt-0.5 text-xs"
|
|
||||||
[class.italic]="!att.requestError"
|
|
||||||
[class.opacity-70]="!att.requestError"
|
|
||||||
[class.text-destructive]="!!att.requestError"
|
|
||||||
[class.text-muted-foreground]="!att.requestError"
|
|
||||||
>
|
|
||||||
{{ att.requestError || 'Waiting for image source...' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
(click)="retryImageRequest(att)"
|
|
||||||
class="mt-2 w-full rounded-md bg-secondary px-3 py-1.5 text-xs text-foreground transition-colors hover:bg-secondary/80"
|
|
||||||
>
|
>
|
||||||
Retry
|
<img
|
||||||
</button>
|
[src]="att.objectUrl"
|
||||||
</div>
|
[alt]="att.filename"
|
||||||
}
|
class="max-h-80 w-auto cursor-pointer rounded-md"
|
||||||
} @else if (att.isVideo || att.isAudio) {
|
(click)="openLightbox(att)"
|
||||||
@if (att.available && att.objectUrl) {
|
/>
|
||||||
@if (att.isVideo) {
|
<div class="pointer-events-none absolute inset-0 rounded-md bg-black/0 transition-colors group-hover/img:bg-black/20"></div>
|
||||||
<app-chat-video-player
|
<div class="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover/img:opacity-100">
|
||||||
[src]="att.objectUrl"
|
<button
|
||||||
[filename]="att.filename"
|
(click)="openLightbox(att); $event.stopPropagation()"
|
||||||
[sizeLabel]="formatBytes(att.size)"
|
class="grid h-7 w-7 place-items-center rounded-md bg-black/60 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
||||||
(downloadRequested)="downloadAttachment(att)"
|
title="View full size"
|
||||||
/>
|
|
||||||
} @else {
|
|
||||||
<app-chat-audio-player
|
|
||||||
[src]="att.objectUrl"
|
|
||||||
[filename]="att.filename"
|
|
||||||
[sizeLabel]="formatBytes(att.size)"
|
|
||||||
(downloadRequested)="downloadAttachment(att)"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
} @else if ((att.receivedBytes || 0) > 0) {
|
|
||||||
<div
|
|
||||||
appThemeNode="chatAttachmentCard"
|
|
||||||
class="max-w-xl rounded-md border border-border bg-secondary/40 p-3"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-3">
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<div class="truncate text-sm font-medium">{{ att.filename }}</div>
|
|
||||||
<div class="text-xs text-muted-foreground">{{ formatBytes(att.receivedBytes || 0) }} / {{ formatBytes(att.size) }}</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="rounded bg-destructive px-2 py-1 text-xs text-destructive-foreground"
|
|
||||||
(click)="cancelAttachment(att)"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full bg-primary transition-all duration-300"
|
|
||||||
[style.width.%]="att.progressPercent"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 flex items-center justify-between text-xs text-muted-foreground">
|
|
||||||
<span>{{ att.progressPercent | number: '1.0-0' }}%</span>
|
|
||||||
@if (att.speedBps) {
|
|
||||||
<span>{{ formatSpeed(att.speedBps) }}</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
<div
|
|
||||||
appThemeNode="chatAttachmentCard"
|
|
||||||
class="max-w-xl rounded-md border border-dashed border-border bg-secondary/20 p-4"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-3">
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<div class="truncate text-sm font-medium text-foreground">{{ att.filename }}</div>
|
|
||||||
<div class="text-xs text-muted-foreground">{{ formatBytes(att.size) }}</div>
|
|
||||||
<div
|
|
||||||
class="mt-1 text-xs leading-relaxed"
|
|
||||||
[class.opacity-80]="!att.requestError"
|
|
||||||
[class.text-destructive]="!!att.requestError"
|
|
||||||
[class.text-muted-foreground]="!att.requestError"
|
|
||||||
>
|
>
|
||||||
{{ att.mediaStatusText }}
|
<ng-icon
|
||||||
|
name="lucideExpand"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
(click)="downloadAttachment(att); $event.stopPropagation()"
|
||||||
|
class="grid h-7 w-7 place-items-center rounded-md bg-black/60 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
||||||
|
title="Download"
|
||||||
|
>
|
||||||
|
<ng-icon
|
||||||
|
name="lucideDownload"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else if ((att.receivedBytes || 0) > 0) {
|
||||||
|
<div
|
||||||
|
appThemeNode="chatAttachmentCard"
|
||||||
|
class="max-w-xs rounded-md border border-border bg-secondary/40 p-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md bg-primary/10">
|
||||||
|
<ng-icon
|
||||||
|
name="lucideImage"
|
||||||
|
class="h-5 w-5 text-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="truncate text-sm font-medium">{{ att.filename }}</div>
|
||||||
|
<div class="text-xs text-muted-foreground">{{ formatBytes(att.receivedBytes || 0) }} / {{ formatBytes(att.size) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs font-medium text-primary">{{ ((att.receivedBytes || 0) * 100) / att.size | number: '1.0-0' }}%</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full bg-primary transition-all duration-300"
|
||||||
|
[style.width.%]="((att.receivedBytes || 0) * 100) / att.size"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div
|
||||||
|
appThemeNode="chatAttachmentCard"
|
||||||
|
class="max-w-xs rounded-md border border-dashed border-border bg-secondary/20 p-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md bg-muted">
|
||||||
|
<ng-icon
|
||||||
|
name="lucideImage"
|
||||||
|
class="h-5 w-5 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="truncate text-sm font-medium text-foreground">{{ att.filename }}</div>
|
||||||
|
<div class="text-xs text-muted-foreground">{{ formatBytes(att.size) }}</div>
|
||||||
|
<div
|
||||||
|
class="mt-0.5 text-xs"
|
||||||
|
[class.italic]="!att.requestError"
|
||||||
|
[class.opacity-70]="!att.requestError"
|
||||||
|
[class.text-destructive]="!!att.requestError"
|
||||||
|
[class.text-muted-foreground]="!att.requestError"
|
||||||
|
>
|
||||||
|
{{ att.requestError || 'Waiting for image source...' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
(click)="requestAttachment(att)"
|
(click)="retryImageRequest(att)"
|
||||||
class="shrink-0 rounded-md bg-secondary px-3 py-1.5 text-xs text-foreground transition-colors hover:bg-secondary/80"
|
class="mt-2 w-full rounded-md bg-secondary px-3 py-1.5 text-xs text-foreground transition-colors hover:bg-secondary/80"
|
||||||
>
|
>
|
||||||
{{ att.mediaActionLabel }}
|
Retry
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
} @else if (att.isVideo || att.isAudio) {
|
||||||
} @else {
|
@if (att.available && att.objectUrl) {
|
||||||
<div
|
@if (att.isVideo) {
|
||||||
appThemeNode="chatAttachmentCard"
|
<app-chat-video-player
|
||||||
class="rounded-md border border-border bg-secondary/40 p-2"
|
[src]="att.objectUrl"
|
||||||
>
|
[filename]="att.filename"
|
||||||
<div class="flex items-center justify-between">
|
[sizeLabel]="formatBytes(att.size)"
|
||||||
<div class="min-w-0">
|
(downloadRequested)="downloadAttachment(att)"
|
||||||
<div class="truncate text-sm font-medium">{{ att.filename }}</div>
|
/>
|
||||||
<div class="text-xs text-muted-foreground">{{ formatBytes(att.size) }}</div>
|
} @else {
|
||||||
|
<app-chat-audio-player
|
||||||
|
[src]="att.objectUrl"
|
||||||
|
[filename]="att.filename"
|
||||||
|
[sizeLabel]="formatBytes(att.size)"
|
||||||
|
(downloadRequested)="downloadAttachment(att)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
} @else if ((att.receivedBytes || 0) > 0) {
|
||||||
|
<div
|
||||||
|
appThemeNode="chatAttachmentCard"
|
||||||
|
class="max-w-xl rounded-md border border-border bg-secondary/40 p-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="truncate text-sm font-medium">{{ att.filename }}</div>
|
||||||
|
<div class="text-xs text-muted-foreground">{{ formatBytes(att.receivedBytes || 0) }} / {{ formatBytes(att.size) }}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="rounded bg-destructive px-2 py-1 text-xs text-destructive-foreground"
|
||||||
|
(click)="cancelAttachment(att)"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full bg-primary transition-all duration-300"
|
||||||
|
[style.width.%]="att.progressPercent"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex items-center justify-between text-xs text-muted-foreground">
|
||||||
|
<span>{{ att.progressPercent | number: '1.0-0' }}%</span>
|
||||||
|
@if (att.speedBps) {
|
||||||
|
<span>{{ formatSpeed(att.speedBps) }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
} @else {
|
||||||
@if (!att.isUploader) {
|
<div
|
||||||
@if (!att.available) {
|
appThemeNode="chatAttachmentCard"
|
||||||
<div class="h-1.5 w-24 rounded bg-muted">
|
class="max-w-xl rounded-md border border-dashed border-border bg-secondary/20 p-4"
|
||||||
<div
|
>
|
||||||
class="h-1.5 rounded bg-primary"
|
<div class="flex items-start justify-between gap-3">
|
||||||
[style.width.%]="att.progressPercent"
|
<div class="min-w-0 flex-1">
|
||||||
></div>
|
<div class="truncate text-sm font-medium text-foreground">{{ att.filename }}</div>
|
||||||
|
<div class="text-xs text-muted-foreground">{{ formatBytes(att.size) }}</div>
|
||||||
|
<div
|
||||||
|
class="mt-1 text-xs leading-relaxed"
|
||||||
|
[class.opacity-80]="!att.requestError"
|
||||||
|
[class.text-destructive]="!!att.requestError"
|
||||||
|
[class.text-muted-foreground]="!att.requestError"
|
||||||
|
>
|
||||||
|
{{ att.mediaStatusText }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
</div>
|
||||||
<span>{{ att.progressPercent | number: '1.0-0' }}%</span>
|
<button
|
||||||
@if (att.speedBps) {
|
(click)="requestAttachment(att)"
|
||||||
<span>• {{ formatSpeed(att.speedBps) }}</span>
|
class="shrink-0 rounded-md bg-secondary px-3 py-1.5 text-xs text-foreground transition-colors hover:bg-secondary/80"
|
||||||
|
>
|
||||||
|
{{ att.mediaActionLabel }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<div
|
||||||
|
appThemeNode="chatAttachmentCard"
|
||||||
|
class="rounded-md border border-border bg-secondary/40 p-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="truncate text-sm font-medium">{{ att.filename }}</div>
|
||||||
|
<div class="text-xs text-muted-foreground">{{ formatBytes(att.size) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
@if (!att.isUploader) {
|
||||||
|
@if (!att.available) {
|
||||||
|
<div class="h-1.5 w-24 rounded bg-muted">
|
||||||
|
<div
|
||||||
|
class="h-1.5 rounded bg-primary"
|
||||||
|
[style.width.%]="att.progressPercent"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>{{ att.progressPercent | number: '1.0-0' }}%</span>
|
||||||
|
@if (att.speedBps) {
|
||||||
|
<span>• {{ formatSpeed(att.speedBps) }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (!(att.receivedBytes || 0)) {
|
||||||
|
<button
|
||||||
|
class="rounded bg-secondary px-2 py-1 text-xs text-foreground"
|
||||||
|
(click)="requestAttachment(att)"
|
||||||
|
>
|
||||||
|
{{ att.requestError ? 'Retry' : 'Request' }}
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
class="rounded bg-destructive px-2 py-1 text-xs text-destructive-foreground"
|
||||||
|
(click)="cancelAttachment(att)"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
@if (!(att.receivedBytes || 0)) {
|
|
||||||
<button
|
|
||||||
class="rounded bg-secondary px-2 py-1 text-xs text-foreground"
|
|
||||||
(click)="requestAttachment(att)"
|
|
||||||
>
|
|
||||||
{{ att.requestError ? 'Retry' : 'Request' }}
|
|
||||||
</button>
|
|
||||||
} @else {
|
} @else {
|
||||||
|
@if (att.canOpenExternally) {
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-1.5 rounded bg-secondary px-2 py-1 text-xs text-foreground transition-colors hover:bg-secondary/80"
|
||||||
|
(click)="openAttachmentExternally(att)"
|
||||||
|
>
|
||||||
|
<ng-icon
|
||||||
|
name="lucideExternalLink"
|
||||||
|
class="h-3.5 w-3.5"
|
||||||
|
/>
|
||||||
|
Open
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (att.canUseExperimentalPlayer) {
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-1.5 rounded bg-secondary px-2 py-1 text-xs text-foreground transition-colors hover:bg-secondary/80"
|
||||||
|
(click)="openExperimentalPlayer(att)"
|
||||||
|
>
|
||||||
|
<ng-icon
|
||||||
|
name="lucidePlay"
|
||||||
|
class="h-3.5 w-3.5"
|
||||||
|
/>
|
||||||
|
Play
|
||||||
|
</button>
|
||||||
|
}
|
||||||
<button
|
<button
|
||||||
class="rounded bg-destructive px-2 py-1 text-xs text-destructive-foreground"
|
class="rounded bg-primary px-2 py-1 text-xs text-primary-foreground"
|
||||||
(click)="cancelAttachment(att)"
|
(click)="downloadAttachment(att)"
|
||||||
>
|
>
|
||||||
Cancel
|
Download
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
} @else {
|
} @else {
|
||||||
|
<div class="text-xs text-muted-foreground">Shared from your device</div>
|
||||||
@if (att.canOpenExternally) {
|
@if (att.canOpenExternally) {
|
||||||
<button
|
<button
|
||||||
class="inline-flex items-center gap-1.5 rounded bg-secondary px-2 py-1 text-xs text-foreground transition-colors hover:bg-secondary/80"
|
class="inline-flex items-center gap-1.5 rounded bg-secondary px-2 py-1 text-xs text-foreground transition-colors hover:bg-secondary/80"
|
||||||
@@ -465,68 +500,35 @@
|
|||||||
Play
|
Play
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button
|
|
||||||
class="rounded bg-primary px-2 py-1 text-xs text-primary-foreground"
|
|
||||||
(click)="downloadAttachment(att)"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
} @else {
|
</div>
|
||||||
<div class="text-xs text-muted-foreground">Shared from your device</div>
|
|
||||||
@if (att.canOpenExternally) {
|
|
||||||
<button
|
|
||||||
class="inline-flex items-center gap-1.5 rounded bg-secondary px-2 py-1 text-xs text-foreground transition-colors hover:bg-secondary/80"
|
|
||||||
(click)="openAttachmentExternally(att)"
|
|
||||||
>
|
|
||||||
<ng-icon
|
|
||||||
name="lucideExternalLink"
|
|
||||||
class="h-3.5 w-3.5"
|
|
||||||
/>
|
|
||||||
Open
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
@if (att.canUseExperimentalPlayer) {
|
|
||||||
<button
|
|
||||||
class="inline-flex items-center gap-1.5 rounded bg-secondary px-2 py-1 text-xs text-foreground transition-colors hover:bg-secondary/80"
|
|
||||||
(click)="openExperimentalPlayer(att)"
|
|
||||||
>
|
|
||||||
<ng-icon
|
|
||||||
name="lucidePlay"
|
|
||||||
class="h-3.5 w-3.5"
|
|
||||||
/>
|
|
||||||
Play
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
@if (!att.available && att.requestError) {
|
||||||
|
<div
|
||||||
|
class="mt-2 w-full rounded-md border border-destructive/20 bg-destructive/5 px-2.5 py-1.5 text-xs leading-relaxed text-destructive"
|
||||||
|
>
|
||||||
|
{{ att.requestError }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (!att.available && att.requestError) {
|
@if (att.experimentalPlayerActive && att.objectUrl) {
|
||||||
<div
|
@defer {
|
||||||
class="mt-2 w-full rounded-md border border-destructive/20 bg-destructive/5 px-2.5 py-1.5 text-xs leading-relaxed text-destructive"
|
<app-experimental-vlc-player
|
||||||
>
|
[src]="att.objectUrl"
|
||||||
{{ att.requestError }}
|
[filename]="att.filename"
|
||||||
</div>
|
[mime]="att.mime"
|
||||||
}
|
[sizeLabel]="formatBytes(att.size)"
|
||||||
</div>
|
(closed)="closeExperimentalPlayer()"
|
||||||
@if (att.experimentalPlayerActive && att.objectUrl) {
|
(downloadRequested)="downloadAttachment(att)"
|
||||||
@defer {
|
/>
|
||||||
<app-experimental-vlc-player
|
} @loading {
|
||||||
[src]="att.objectUrl"
|
<div class="mt-2 max-w-xl rounded-md border border-border bg-secondary/20 p-3 text-xs text-muted-foreground">
|
||||||
[filename]="att.filename"
|
Loading experimental player...
|
||||||
[mime]="att.mime"
|
</div>
|
||||||
[sizeLabel]="formatBytes(att.size)"
|
}
|
||||||
(closed)="closeExperimentalPlayer()"
|
|
||||||
(downloadRequested)="downloadAttachment(att)"
|
|
||||||
/>
|
|
||||||
} @loading {
|
|
||||||
<div class="mt-2 max-w-xl rounded-md border border-border bg-secondary/20 p-3 text-xs text-muted-foreground">
|
|
||||||
Loading experimental player...
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<nav class="relative flex h-full w-16 min-w-16 max-w-16 flex-col items-center gap-2 border-r border-border bg-secondary/35 px-0 py-3 md:min-w-0 md:max-w-none md:w-full">
|
<nav
|
||||||
|
class="relative flex h-full w-16 min-w-16 max-w-16 flex-col items-center gap-2 border-r border-border bg-secondary/35 px-0 py-3 md:min-w-0 md:max-w-none md:w-full"
|
||||||
|
>
|
||||||
<!-- Home / dashboard button -->
|
<!-- Home / dashboard button -->
|
||||||
<button
|
<button
|
||||||
appThemeNode="serversRailCreateButton"
|
appThemeNode="serversRailCreateButton"
|
||||||
|
|||||||
@@ -31,6 +31,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@if (isElectron) {
|
||||||
|
<section class="rounded-lg border border-border bg-secondary/20 px-4 py-3">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<ng-icon
|
||||||
|
name="lucideMemoryStick"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">Process RAM</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-mono text-sm text-foreground">{{ ramLabel() ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">Live total working set from Electron app metrics. Updates every 2 seconds.</p>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
<section class="grid gap-3 sm:grid-cols-3">
|
<section class="grid gap-3 sm:grid-cols-3">
|
||||||
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<div class="flex items-center gap-2 text-muted-foreground">
|
<div class="flex items-center gap-2 text-muted-foreground">
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/member-ordering */
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
DestroyRef,
|
||||||
computed,
|
computed,
|
||||||
inject
|
inject,
|
||||||
|
signal
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||||
import {
|
import {
|
||||||
lucideBug,
|
lucideBug,
|
||||||
lucideCircleAlert,
|
lucideCircleAlert,
|
||||||
lucideClock3,
|
lucideClock3,
|
||||||
|
lucideMemoryStick,
|
||||||
lucideTrash2,
|
lucideTrash2,
|
||||||
lucideTriangleAlert
|
lucideTriangleAlert
|
||||||
} from '@ng-icons/lucide';
|
} from '@ng-icons/lucide';
|
||||||
|
import { interval } from 'rxjs';
|
||||||
|
import { startWith, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DebuggingService } from '../../../../core/services/debugging.service';
|
import { DebuggingService } from '../../../../core/services/debugging.service';
|
||||||
|
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||||
|
import { formatAppRamLabel } from '../../../../core/platform/electron/electron-app-metrics.rules';
|
||||||
|
import { PlatformService } from '../../../../core/platform';
|
||||||
|
|
||||||
|
const APP_METRICS_POLL_INTERVAL_MS = 2_000;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-debugging-settings',
|
selector: 'app-debugging-settings',
|
||||||
@@ -24,6 +36,7 @@ import { DebuggingService } from '../../../../core/services/debugging.service';
|
|||||||
lucideBug,
|
lucideBug,
|
||||||
lucideCircleAlert,
|
lucideCircleAlert,
|
||||||
lucideClock3,
|
lucideClock3,
|
||||||
|
lucideMemoryStick,
|
||||||
lucideTrash2,
|
lucideTrash2,
|
||||||
lucideTriangleAlert
|
lucideTriangleAlert
|
||||||
})
|
})
|
||||||
@@ -31,8 +44,13 @@ import { DebuggingService } from '../../../../core/services/debugging.service';
|
|||||||
templateUrl: './debugging-settings.component.html'
|
templateUrl: './debugging-settings.component.html'
|
||||||
})
|
})
|
||||||
export class DebuggingSettingsComponent {
|
export class DebuggingSettingsComponent {
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly platform = inject(PlatformService);
|
||||||
|
private readonly electronBridge = inject(ElectronBridgeService);
|
||||||
readonly debugging = inject(DebuggingService);
|
readonly debugging = inject(DebuggingService);
|
||||||
|
|
||||||
|
readonly isElectron = this.platform.isElectron;
|
||||||
|
readonly ramLabel = signal<string | null>(null);
|
||||||
readonly enabled = this.debugging.enabled;
|
readonly enabled = this.debugging.enabled;
|
||||||
readonly isConsoleOpen = this.debugging.isConsoleOpen;
|
readonly isConsoleOpen = this.debugging.isConsoleOpen;
|
||||||
readonly entryCount = computed(() => {
|
readonly entryCount = computed(() => {
|
||||||
@@ -54,6 +72,11 @@ export class DebuggingSettingsComponent {
|
|||||||
return lastEntry ? lastEntry.timeLabel : 'No logs yet';
|
return lastEntry ? lastEntry.timeLabel : 'No logs yet';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (this.isElectron)
|
||||||
|
this.startRamPolling();
|
||||||
|
}
|
||||||
|
|
||||||
onEnabledChange(event: Event): void {
|
onEnabledChange(event: Event): void {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
|
|
||||||
@@ -67,4 +90,26 @@ export class DebuggingSettingsComponent {
|
|||||||
clearLogs(): void {
|
clearLogs(): void {
|
||||||
this.debugging.clear();
|
this.debugging.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startRamPolling(): void {
|
||||||
|
const api = this.electronBridge.getApi();
|
||||||
|
|
||||||
|
if (!api?.getAppMetrics)
|
||||||
|
return;
|
||||||
|
|
||||||
|
interval(APP_METRICS_POLL_INTERVAL_MS)
|
||||||
|
.pipe(
|
||||||
|
startWith(0),
|
||||||
|
switchMap(() => api.getAppMetrics()),
|
||||||
|
takeUntilDestroyed(this.destroyRef)
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (snapshot) => {
|
||||||
|
this.ramLabel.set(formatAppRamLabel(snapshot));
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.ramLabel.set(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user