mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
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:
+25
-1
@@ -3720,6 +3720,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
static LOG_PAUSED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
#[tauri::command]
|
||||
fn toggle_log_pause(pause: bool) {
|
||||
LOG_PAUSED.store(pause, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn is_log_paused() -> bool {
|
||||
LOG_PAUSED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let extension_pairing_token = Arc::new(RwLock::new(String::new()));
|
||||
@@ -3746,6 +3758,17 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.manage(Aria2DaemonGuard::new())
|
||||
.setup(move |app| {
|
||||
let mut sys = sysinfo::System::new_all();
|
||||
sys.refresh_all();
|
||||
log::info!("=== System Information ===");
|
||||
log::info!("OS: {} {}", sysinfo::System::name().unwrap_or_else(|| "Unknown".to_string()), sysinfo::System::os_version().unwrap_or_else(|| "Unknown".to_string()));
|
||||
let arch = sysinfo::System::cpu_arch();
|
||||
log::info!("Architecture: {}", if arch.is_empty() { "Unknown" } else { &arch });
|
||||
log::info!("CPU: {} ({} cores)", sys.cpus().first().map(|c| c.brand()).unwrap_or("Unknown"), sys.cpus().len());
|
||||
log::info!("Memory: {} MB total", sys.total_memory() / 1024 / 1024);
|
||||
log::info!("App Version: {}", env!("CARGO_PKG_VERSION"));
|
||||
log::info!("==========================");
|
||||
|
||||
build_main_tray(app.handle())
|
||||
.map_err(|error| format!("failed to create tray menu: {error}"))?;
|
||||
|
||||
@@ -4052,6 +4075,7 @@ pub fn run() {
|
||||
})
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::new()
|
||||
.filter(|_meta| !LOG_PAUSED.load(std::sync::atomic::Ordering::Relaxed))
|
||||
.targets([
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }),
|
||||
@@ -4092,7 +4116,7 @@ pub fn run() {
|
||||
parity::create_category_directories,
|
||||
db_save_settings, db_load_settings, db_get_all_downloads, db_replace_downloads,
|
||||
db_get_all_queues, db_replace_queues,
|
||||
read_logs, export_logs
|
||||
read_logs, export_logs, toggle_log_pause, is_log_paused
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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:', '');
|
||||
|
||||
@@ -69,6 +69,8 @@ type CommandMap = {
|
||||
};
|
||||
export_logs: { args: { destPath: string }; result: string };
|
||||
read_logs: { args: { limit: number }; result: string[] };
|
||||
toggle_log_pause: { args: { pause: boolean }; result: void };
|
||||
is_log_paused: { args: undefined; result: boolean };
|
||||
get_pending_order: { args: { queueId: string | null }; result: string[] };
|
||||
enqueue_download: { args: { item: EnqueueItem }; result: EnqueueAccepted };
|
||||
enqueue_many: { args: { items: EnqueueItem[] }; result: import('./bindings/EnqueueResult').EnqueueResult[] };
|
||||
|
||||
Reference in New Issue
Block a user