feat: optimize log viewer, add global item deletion, and system telemetry

- Replaced LogViewer setInterval polling with reactive attachLogger
- Added backend LOG_PAUSED toggle to fully halt logs and save I/O
- Added native text selection to logs console
- Implemented Backspace/Delete keyboard shortcuts across DownloadTable and Sidebar with modal-aware safeties
- Integrated cross-platform sysinfo logging for telemetry
This commit is contained in:
NimBold
2026-06-22 21:20:52 +03:30
parent 7c835b022f
commit c5ee3ec3cd
5 changed files with 128 additions and 23 deletions
+18
View File
@@ -88,6 +88,24 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
window.localStorage.setItem(COLUMN_WIDTHS_STORAGE_KEY, JSON.stringify(columnWidths));
}, [columnWidths]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (document.querySelector('.app-modal-backdrop') || document.querySelector('.app-modal')) return;
const activeEl = document.activeElement as HTMLElement | null;
const isInput = activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable);
if (!isInput && (e.key === 'Delete' || e.key === 'Backspace')) {
if (!activeEl || !activeEl.closest('.sidebar-inner')) {
if (selectedIds.size > 0) {
handleDelete(Array.from(selectedIds));
}
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [selectedIds]);
const showInteractionError = (message: string, error: unknown) => {
const detail = error instanceof Error ? error.message : String(error);
+59 -22
View File
@@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from 'react';
import { invokeCommand as invoke } from '../ipc';
import { save } from '@tauri-apps/plugin-dialog';
import { FileDown, Trash2, Terminal, Filter } from 'lucide-react';
import { attachLogger } from '@tauri-apps/plugin-log';
import { FileDown, Trash2, Terminal, Filter, Play, Pause } from 'lucide-react';
import { WindowDragRegion } from './WindowDragRegion';
import { useToast } from '../contexts/ToastContext';
@@ -14,42 +15,68 @@ export default function LogsView() {
const { addToast } = useToast();
const [logs, setLogs] = useState<LogEntry[]>([]);
const [levelFilter, setLevelFilter] = useState<LogEntry['level'] | 'All'>('All');
const [isPaused, setIsPaused] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const rawLineCountRef = useRef(0);
const clearedThroughRef = useRef(0);
const lastSnapshotRef = useRef('');
const MAX_LOG_LINES = 2000;
useEffect(() => {
let active = true;
const refresh = async () => {
let unlisten: (() => void) | undefined;
const init = async () => {
try {
const lines = await invoke('read_logs', { limit: MAX_LOG_LINES });
const [lines, currentPauseState] = await Promise.all([
invoke('read_logs', { limit: MAX_LOG_LINES }),
invoke('is_log_paused')
]);
if (!active) return;
if (lines.length < clearedThroughRef.current) {
clearedThroughRef.current = 0;
}
const snapshot = `${lines.length}:${lines[lines.length - 1] || ''}`;
if (snapshot === lastSnapshotRef.current) return;
lastSnapshotRef.current = snapshot;
rawLineCountRef.current = lines.length;
setLogs(lines.slice(clearedThroughRef.current).map(message => {
const level = message.includes('[ERROR]') ? 'Error'
setIsPaused(currentPauseState);
if (!active) return;
const initialLogs = lines.map(message => {
const level: LogEntry['level'] = message.includes('[ERROR]') ? 'Error'
: message.includes('[WARN]') ? 'Warn'
: message.includes('[INFO]') ? 'Info'
: message.includes('[TRACE]') ? 'Trace'
: 'Debug';
return { level, message };
}));
} catch {
if (active) setLogs([]);
});
setLogs(initialLogs);
rawLineCountRef.current = initialLogs.length;
unlisten = await attachLogger((log) => {
if (!active) return;
const levelStr: LogEntry['level'] = log.level === 5 ? 'Error'
: log.level === 4 ? 'Warn'
: log.level === 3 ? 'Info'
: log.level === 1 ? 'Trace'
: 'Debug';
const timeStr = new Date().toISOString().replace('T', ' ').substring(0, 19);
const formattedMsg = `[${timeStr}] [${levelStr.toUpperCase()}] ${log.message}`;
setLogs(prev => {
const newLogs = [...prev, { level: levelStr, message: formattedMsg }];
rawLineCountRef.current = newLogs.length;
if (newLogs.length > MAX_LOG_LINES + 500) {
const trimmed = newLogs.slice(newLogs.length - MAX_LOG_LINES);
return trimmed;
}
return newLogs;
});
});
} catch (e) {
console.error('Failed to init logs:', e);
}
};
void refresh();
const interval = window.setInterval(refresh, 2000);
void init();
return () => {
active = false;
window.clearInterval(interval);
if (unlisten) unlisten();
};
}, []);
@@ -75,7 +102,6 @@ export default function LogsView() {
};
const handleClear = () => {
clearedThroughRef.current = rawLineCountRef.current;
setLogs([]);
};
@@ -116,6 +142,17 @@ export default function LogsView() {
</select>
</div>
<div className="w-[1px] h-4 bg-border-modal mx-0.5" />
<button
onClick={async () => {
const newState = !isPaused;
setIsPaused(newState);
await invoke('toggle_log_pause', { pause: newState }).catch(console.error);
}}
className={`app-icon-button ${isPaused ? 'text-accent' : ''}`}
title={isPaused ? "Resume logs" : "Pause logs system"}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
onClick={handleClear}
className="app-icon-button"
@@ -135,7 +172,7 @@ export default function LogsView() {
</div>
{/* Console */}
<div ref={scrollRef} className="logs-console flex-1 overflow-y-auto p-3 font-mono text-[11px] leading-[1.5]">
<div ref={scrollRef} className="logs-console flex-1 overflow-y-auto p-3 font-mono text-[11px] leading-[1.5] select-text" style={{ userSelect: 'text', WebkitUserSelect: 'text' }}>
{logs.length === 0 && (
<div className="text-text-muted italic select-none">No persisted log entries are available yet.</div>
)}
+24
View File
@@ -53,6 +53,30 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
if (renamingQueueId) renameInputRef.current?.focus();
}, [renamingQueueId]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (document.querySelector('.app-modal-backdrop') || document.querySelector('.app-modal')) return;
const activeEl = document.activeElement as HTMLElement | null;
const isInput = activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable);
if (!isInput && (e.key === 'Delete' || e.key === 'Backspace')) {
if (activeEl && activeEl.closest('.sidebar-inner')) {
if (activeView === 'downloads' && selectedFilter.startsWith('queue:')) {
const queueId = selectedFilter.replace('queue:', '');
const q = queues.find(q => q.id === queueId);
if (q && !q.isMain) {
void removeQueue(queueId).catch(error => {
addToast({ message: `Could not delete queue: ${String(error)}`, variant: 'error', isActionable: true });
});
}
}
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [selectedFilter, activeView, queues]);
const getCount = (filter: SidebarFilter) => {
if (filter.startsWith('queue:')) {
const qid = filter.replace('queue:', '');