diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fe1ef48..e4a5820 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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") diff --git a/src/components/DownloadTable.tsx b/src/components/DownloadTable.tsx index 541e3f5..29b0860 100644 --- a/src/components/DownloadTable.tsx +++ b/src/components/DownloadTable.tsx @@ -88,6 +88,24 @@ export const DownloadTable: React.FC = ({ 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); diff --git a/src/components/LogsView.tsx b/src/components/LogsView.tsx index eaf789e..cb4d27d 100644 --- a/src/components/LogsView.tsx +++ b/src/components/LogsView.tsx @@ -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([]); const [levelFilter, setLevelFilter] = useState('All'); + const [isPaused, setIsPaused] = useState(false); const scrollRef = useRef(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() {
+