""" 主窗口 """ import os from typing import Optional from PyQt5.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QStatusBar, QMessageBox, QApplication, QSplitter, QFrame ) from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtGui import QFont, QCloseEvent from disk_manager import DiskManager, Partition from file_scanner import FileScanner, FileItem, ScanProgress from utils import format_size, check_sudo_needs_password, verify_sudo_password, safe_delete from gui.partition_dialog import PartitionDialog from gui.file_browser import FileBrowser from gui.confirm_dialog import LogDeleteConfirmDialog from gui.sudo_dialog import SudoDialog class DirectorySizeThread(QThread): """目录大小统计线程""" progress = pyqtSignal(object) # ScanProgress finished = pyqtSignal(dict) # {path: size} error = pyqtSignal(str) def __init__(self, scanner: FileScanner, path: str): super().__init__() self.scanner = scanner self.path = path def run(self): try: sizes = self.scanner.scan_directory_sizes( self.path, callback=lambda p: self.progress.emit(p) ) self.finished.emit(sizes) except Exception as e: self.error.emit(str(e)) class MainWindow(QMainWindow): """主窗口""" def __init__(self, sudo_password: Optional[str] = None): super().__init__() self.sudo_password = sudo_password self.disk_manager = DiskManager(sudo_password) self.file_scanner = FileScanner(sudo_password) self.current_partition: Optional[Partition] = None self.current_mount_point: Optional[str] = None self.dir_sizes = {} # {path: size} self.init_ui() def init_ui(self): """初始化界面""" self.setWindowTitle("Linux 日志删除助手") self.setMinimumSize(900, 600) # 中央部件 central_widget = QWidget() self.setCentralWidget(central_widget) # 主布局 main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(10, 10, 10, 10) main_layout.setSpacing(10) # 顶部信息栏 info_layout = QHBoxLayout() # 分区信息 self.partition_label = QLabel("当前分区:未选择") font = QFont() font.setPointSize(11) self.partition_label.setFont(font) info_layout.addWidget(self.partition_label) info_layout.addStretch() # 切换分区按钮 self.switch_partition_button = QPushButton("🔄 切换分区") self.switch_partition_button.clicked.connect(self.switch_partition) info_layout.addWidget(self.switch_partition_button) main_layout.addLayout(info_layout) # 工具栏 toolbar_layout = QHBoxLayout() # 刷新按钮 self.refresh_button = QPushButton("🔄 刷新") self.refresh_button.clicked.connect(self.refresh) toolbar_layout.addWidget(self.refresh_button) # 统计按钮 self.scan_button = QPushButton("📊 统计目录大小") self.scan_button.clicked.connect(self.scan_directory_sizes) toolbar_layout.addWidget(self.scan_button) # 快捷删除日志按钮 self.delete_logs_button = QPushButton("🗑️ 快捷删除系统日志") self.delete_logs_button.setStyleSheet( "QPushButton { background-color: #e74c3c; color: white; font-weight: bold; }" ) self.delete_logs_button.clicked.connect(self.quick_delete_logs) self.delete_logs_button.hide() # 默认隐藏,检测到大日志时显示 toolbar_layout.addWidget(self.delete_logs_button) toolbar_layout.addStretch() main_layout.addLayout(toolbar_layout) # 分隔线 separator = QFrame() separator.setFrameStyle(QFrame.HLine | QFrame.Sunken) main_layout.addWidget(separator) # 文件浏览器 self.file_browser = FileBrowser( self.file_scanner, self.sudo_password, self ) self.file_browser.directory_changed.connect(self.on_directory_changed) self.file_browser.files_deleted.connect(self.on_files_deleted) main_layout.addWidget(self.file_browser) # 状态栏 self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.status_bar.showMessage("就绪") # 进度条(用于扫描) self.progress_bar = QLabel() self.status_bar.addPermanentWidget(self.progress_bar) # 默认禁用工具栏按钮 self.refresh_button.setEnabled(False) self.scan_button.setEnabled(False) def switch_partition(self): """切换分区""" dialog = PartitionDialog(self.disk_manager, self) if dialog.exec_() == PartitionDialog.Accepted: partition = dialog.get_selected_partition() if partition: self.load_partition(partition) def load_partition(self, partition: Partition): """加载分区""" self.current_partition = partition self.status_bar.showMessage(f"正在挂载分区 {partition.device}...") # 挂载分区 success, result = self.disk_manager.mount_partition(partition) if success: self.current_mount_point = result # 更新界面 self.partition_label.setText( f"当前分区:{partition.device} " f"({partition.mount_point or '未挂载'}) " f"- {partition.used_formatted} / {partition.size_formatted} " f"({partition.use_percent_formatted})" ) # 启用工具栏按钮 self.refresh_button.setEnabled(True) self.scan_button.setEnabled(True) # 加载根目录 self.file_browser.load_directory(self.current_mount_point) # 检测系统日志 self.check_system_logs() self.status_bar.showMessage(f"已加载分区 {partition.device}") else: QMessageBox.critical( self, "错误", f"挂载分区失败:\n{result}" ) self.status_bar.showMessage("挂载分区失败") def check_system_logs(self): """检测系统日志大小""" if not self.current_mount_point: return logs, total_size = self.file_scanner.get_system_logs(self.current_mount_point) # 如果大于 1GB,显示快捷删除按钮 if total_size > 1024 * 1024 * 1024: self.delete_logs_button.setText( f"🗑️ 快捷删除系统日志 ({format_size(total_size)})" ) self.delete_logs_button.show() self._cached_logs = logs self._cached_logs_size = total_size else: self.delete_logs_button.hide() def quick_delete_logs(self): """快捷删除系统日志""" if not hasattr(self, '_cached_logs') or not self._cached_logs: return dialog = LogDeleteConfirmDialog( self._cached_logs, self._cached_logs_size, self ) if dialog.exec_() == LogDeleteConfirmDialog.Accepted and dialog.is_confirmed(): # 删除日志文件 deleted_size = 0 errors = [] for log_file in self._cached_logs: success, message = safe_delete( log_file.path, use_sudo=True, sudo_password=self.sudo_password ) if success: deleted_size += log_file.size else: errors.append(f"{log_file.name}: {message}") # 显示结果 if errors: QMessageBox.warning( self, "清理完成(有错误)", f"成功清理 {len(self._cached_logs) - len(errors)} 个日志文件,\n" f"释放 {format_size(deleted_size)} 空间。\n\n" f"以下文件清理失败:\n" + "\n".join(errors[:10]) ) else: QMessageBox.information( self, "清理成功", f"成功清理 {len(self._cached_logs)} 个日志文件,\n" f"释放 {format_size(deleted_size)} 空间。" ) # 刷新 self.check_system_logs() self.refresh() def scan_directory_sizes(self): """统计目录大小""" if not self.current_mount_point: return self.scan_button.setEnabled(False) self.status_bar.showMessage("正在统计目录大小...") # 在后台线程统计 self.scan_thread = DirectorySizeThread( self.file_scanner, self.current_mount_point ) self.scan_thread.progress.connect(self.on_scan_progress) self.scan_thread.finished.connect(self.on_scan_finished) self.scan_thread.error.connect(self.on_scan_error) self.scan_thread.start() def on_scan_progress(self, progress: ScanProgress): """扫描进度更新""" self.progress_bar.setText( f"已扫描 {progress.scanned_dirs} 个目录, " f"{progress.scanned_files} 个文件" ) def on_scan_finished(self, sizes: dict): """扫描完成""" self.dir_sizes = sizes self.scan_button.setEnabled(True) self.progress_bar.clear() # 按大小排序 sorted_sizes = sorted( sizes.items(), key=lambda x: x[1], reverse=True ) # 显示结果 total_size = sum(sizes.values()) self.status_bar.showMessage( f"统计完成,共 {len(sizes)} 个目录," f"总大小 {format_size(total_size)}" ) # 可以在这里显示一个统计结果窗口 self.show_scan_results(sorted_sizes) def on_scan_error(self, error_message: str): """扫描出错""" self.scan_button.setEnabled(True) self.progress_bar.clear() self.status_bar.showMessage(f"统计失败:{error_message}") QMessageBox.critical( self, "错误", f"统计目录大小失败:\n{error_message}" ) def show_scan_results(self, sorted_sizes: list): """显示扫描结果""" # TODO: 可以创建一个专门的结果窗口 # 目前先在状态栏显示前几个大目录 if sorted_sizes: top_dirs = sorted_sizes[:5] info = "最大的目录: " + ", ".join( f"{os.path.basename(p)} ({format_size(s)})" for p, s in top_dirs ) self.status_bar.showMessage(info) def on_directory_changed(self, path: str): """目录改变""" # 可以在这里更新界面状态 pass def on_files_deleted(self, freed_size: int): """文件被删除""" # 更新分区使用情况 if self.current_partition: self.current_partition.used -= freed_size self.current_partition.available += freed_size self.partition_label.setText( f"当前分区:{self.current_partition.device} " f"({self.current_partition.mount_point or '未挂载'}) " f"- {self.current_partition.used_formatted} / " f"{self.current_partition.size_formatted} " f"({self.current_partition.use_percent_formatted})" ) def refresh(self): """刷新""" self.file_browser.refresh() def closeEvent(self, event: QCloseEvent): """关闭事件""" # 卸载所有分区 reply = QMessageBox.question( self, "确认退出", "确定要退出吗?\n已挂载的分区将被卸载。", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes ) if reply == QMessageBox.Yes: self.disk_manager.unmount_all() event.accept() else: event.ignore()