| |
| |
| """ |
| PyQt6 Storage Cleaner - Full Application |
| A professional disk cleanup utility with dark theme GUI. |
| """ |
|
|
| import os |
| import sys |
| import time |
| import shutil |
| import platform |
| from pathlib import Path |
| from datetime import datetime |
| from dataclasses import dataclass |
| from typing import List, Tuple |
|
|
| from PyQt6.QtWidgets import ( |
| QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, |
| QTabWidget, QLabel, QPushButton, QProgressBar, QCheckBox, |
| QTreeWidget, QTreeWidgetItem, QTableWidget, QTableWidgetItem, |
| QHeaderView, QAbstractItemView, QTextEdit, QLineEdit, |
| QFileDialog, QMessageBox, QStatusBar, QGroupBox, QSplitter, |
| QFrame, QComboBox, QSpinBox, QMenu, QMenuBar |
| ) |
| from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QSize |
| from PyQt6.QtGui import QFont, QColor, QAction, QIcon, QPalette |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class DiskInfo: |
| drive: str |
| mount_point: str |
| total: int |
| used: int |
| free: int |
| percent: float |
| fs_type: str = "" |
|
|
|
|
| @dataclass |
| class FileEntry: |
| path: str |
| size: int |
| modified: str |
| ext: str = "" |
| category: str = "" |
|
|
|
|
| def format_size(size_bytes: int) -> str: |
| for unit in ['B', 'KB', 'MB', 'GB', 'TB']: |
| if size_bytes < 1024: |
| return f"{size_bytes:.1f} {unit}" |
| size_bytes /= 1024 |
| return f"{size_bytes:.1f} PB" |
|
|
|
|
| class DiskScanner: |
| """Scan disk usage and find files.""" |
| |
| LARGE_FILE_THRESHOLD = 50 * 1024 * 1024 |
| |
| TEMP_EXTENSIONS = { |
| '.tmp', '.temp', '.log', '.cache', '.bak', '.old', |
| '.chk', '.dmp', '.sav', '.swp', '.pyc', '.pyo', |
| } |
| |
| CATEGORY_MAP = { |
| 'video': {'.mp4', '.avi', '.mkv', '.mov', '.flv', '.wmv', '.webm', '.m4v'}, |
| 'audio': {'.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma', '.m4a'}, |
| 'image': {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.svg', '.psd'}, |
| 'archive': {'.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.iso'}, |
| 'document': {'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt'}, |
| 'installer': {'.exe', '.msi', '.dmg', '.deb', '.rpm', '.appimage'}, |
| } |
| |
| @classmethod |
| def get_disks(cls) -> List[DiskInfo]: |
| disks = [] |
| if sys.platform == 'win32': |
| import ctypes |
| bitmask = ctypes.windll.kernel32.GetLogicalDrives() |
| for letter in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': |
| if bitmask & 1: |
| drive = f"{letter}:\\" |
| try: |
| usage = shutil.disk_usage(drive) |
| disks.append(DiskInfo( |
| drive=f"{letter}:", |
| mount_point=drive, |
| total=usage.total, |
| used=usage.used, |
| free=usage.free, |
| percent=round(usage.used / usage.total * 100, 1) if usage.total > 0 else 0 |
| )) |
| except (OSError, PermissionError): |
| pass |
| bitmask >>= 1 |
| else: |
| try: |
| usage = shutil.disk_usage('/') |
| disks.append(DiskInfo( |
| drive="/", |
| mount_point="/", |
| total=usage.total, |
| used=usage.used, |
| free=usage.free, |
| percent=round(usage.used / usage.total * 100, 1) |
| )) |
| except (OSError, PermissionError): |
| pass |
| |
| home = str(Path.home()) |
| try: |
| usage = shutil.disk_usage(home) |
| if usage.total != disks[0].total if disks else True: |
| disks.append(DiskInfo( |
| drive=home, |
| mount_point=home, |
| total=usage.total, |
| used=usage.used, |
| free=usage.free, |
| percent=round(usage.used / usage.total * 100, 1) |
| )) |
| except (OSError, PermissionError, IndexError): |
| pass |
| return disks |
| |
| @classmethod |
| def get_category(cls, ext: str) -> str: |
| ext = ext.lower() |
| for cat, exts in cls.CATEGORY_MAP.items(): |
| if ext in exts: |
| return cat |
| return "other" |
| |
| @classmethod |
| def scan_large_files(cls, root: str, threshold: int = None, max_files: int = 200, callback=None) -> List[FileEntry]: |
| if threshold is None: |
| threshold = cls.LARGE_FILE_THRESHOLD |
| results = [] |
| count = 0 |
| for dirpath, dirnames, filenames in os.walk(root): |
| |
| dirnames[:] = [d for d in dirnames if not d.startswith('.') and d not in { |
| '$Recycle.Bin', 'System Volume Information', 'Windows', |
| 'node_modules', '.git', '__pycache__', 'venv', '.venv' |
| }] |
| for fname in filenames: |
| try: |
| fpath = os.path.join(dirpath, fname) |
| size = os.path.getsize(fpath) |
| if size >= threshold: |
| ext = os.path.splitext(fname)[1].lower() |
| try: |
| mtime = datetime.fromtimestamp(os.path.getmtime(fpath)).strftime('%Y-%m-%d %H:%M') |
| except (OSError, ValueError): |
| mtime = "Unknown" |
| results.append(FileEntry( |
| path=fpath, size=size, modified=mtime, |
| ext=ext, category=cls.get_category(ext) |
| )) |
| count += 1 |
| if callback and count % 10 == 0: |
| callback(count) |
| if count >= max_files: |
| return sorted(results, key=lambda x: x.size, reverse=True) |
| except (OSError, PermissionError): |
| continue |
| return sorted(results, key=lambda x: x.size, reverse=True) |
| |
| @classmethod |
| def get_temp_paths(cls) -> List[str]: |
| paths = [] |
| if sys.platform == 'win32': |
| paths.extend([ |
| os.environ.get('TEMP', ''), |
| os.environ.get('TMP', ''), |
| os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Temp'), |
| os.path.join(os.environ.get('WINDIR', r'C:\Windows'), 'Temp'), |
| os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Microsoft', 'Windows', 'INetCache'), |
| os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Microsoft', 'Windows', 'Explorer'), |
| ]) |
| else: |
| paths.extend([ |
| '/tmp', |
| os.path.join(str(Path.home()), '.cache'), |
| os.path.join(str(Path.home()), '.local', 'share', 'Trash'), |
| ]) |
| return [p for p in paths if p and os.path.exists(p)] |
| |
| @classmethod |
| def scan_temp_size(cls) -> Tuple[int, int]: |
| """Return (total_size, file_count) of temp files.""" |
| total_size = 0 |
| file_count = 0 |
| for temp_dir in cls.get_temp_paths(): |
| try: |
| for root, dirs, files in os.walk(temp_dir): |
| for f in files: |
| try: |
| total_size += os.path.getsize(os.path.join(root, f)) |
| file_count += 1 |
| except (OSError, PermissionError): |
| pass |
| except (OSError, PermissionError): |
| pass |
| return total_size, file_count |
|
|
|
|
| |
| |
| |
|
|
| class DiskCleaner: |
| """Clean files from disk.""" |
| |
| @classmethod |
| def delete_files(cls, paths: List[str]) -> dict: |
| result = {'deleted': 0, 'failed': 0, 'freed': 0, 'errors': []} |
| for path in paths: |
| try: |
| if os.path.isfile(path): |
| size = os.path.getsize(path) |
| os.remove(path) |
| result['deleted'] += 1 |
| result['freed'] += size |
| elif os.path.isdir(path): |
| size = sum(os.path.getsize(os.path.join(r, f)) for r, _, fs in os.walk(path) for f in fs) |
| shutil.rmtree(path) |
| result['deleted'] += 1 |
| result['freed'] += size |
| except (OSError, PermissionError) as e: |
| result['failed'] += 1 |
| result['errors'].append(f"{os.path.basename(path)}: {e}") |
| return result |
| |
| @classmethod |
| def clean_temp_files(cls, callback=None) -> dict: |
| result = {'deleted': 0, 'failed': 0, 'freed': 0, 'errors': []} |
| temp_paths = DiskScanner.get_temp_paths() |
| |
| for temp_dir in temp_paths: |
| try: |
| for root, dirs, files in os.walk(temp_dir): |
| for f in files: |
| fpath = os.path.join(root, f) |
| try: |
| size = os.path.getsize(fpath) |
| os.remove(fpath) |
| result['deleted'] += 1 |
| result['freed'] += size |
| if callback: |
| callback(result['deleted']) |
| except (OSError, PermissionError): |
| result['failed'] += 1 |
| except (OSError, PermissionError): |
| pass |
| return result |
| |
| @classmethod |
| def clean_empty_dirs(cls, root: str) -> int: |
| deleted = 0 |
| for dirpath, dirnames, filenames in os.walk(root, topdown=False): |
| if not os.listdir(dirpath) and dirpath != root: |
| try: |
| os.rmdir(dirpath) |
| deleted += 1 |
| except (OSError, PermissionError): |
| pass |
| return deleted |
| |
| @classmethod |
| def empty_recycle_bin(cls) -> bool: |
| if sys.platform == 'win32': |
| try: |
| import ctypes |
| ctypes.windll.shell32.SHEmptyRecycleBinW(None, None, 0x07) |
| return True |
| except Exception: |
| return False |
| else: |
| trash = os.path.join(str(Path.home()), '.local', 'share', 'Trash') |
| if os.path.exists(trash): |
| try: |
| shutil.rmtree(trash) |
| os.makedirs(trash, exist_ok=True) |
| return True |
| except (OSError, PermissionError): |
| return False |
| return False |
|
|
|
|
| |
| |
| |
|
|
| class ScanWorker(QThread): |
| progress = pyqtSignal(int) |
| finished = pyqtSignal(list) |
| status = pyqtSignal(str) |
| |
| def __init__(self, root, threshold=None): |
| super().__init__() |
| self.root = root |
| self.threshold = threshold |
| |
| def run(self): |
| self.status.emit(f"Scanning {self.root}...") |
| results = DiskScanner.scan_large_files( |
| self.root, self.threshold, callback=lambda c: self.progress.emit(c) |
| ) |
| self.finished.emit(results) |
|
|
|
|
| class CleanWorker(QThread): |
| progress = pyqtSignal(int) |
| finished = pyqtSignal(dict) |
| status = pyqtSignal(str) |
| |
| def __init__(self, mode='temp', paths=None): |
| super().__init__() |
| self.mode = mode |
| self.paths = paths or [] |
| |
| def run(self): |
| if self.mode == 'temp': |
| self.status.emit("Cleaning temp files...") |
| result = DiskCleaner.clean_temp_files(callback=lambda c: self.progress.emit(c)) |
| elif self.mode == 'files': |
| self.status.emit("Deleting selected files...") |
| result = DiskCleaner.delete_files(self.paths) |
| elif self.mode == 'recycle': |
| self.status.emit("Emptying recycle bin...") |
| ok = DiskCleaner.empty_recycle_bin() |
| result = {'deleted': 1 if ok else 0, 'failed': 0 if ok else 1, 'freed': 0, 'errors': []} |
| else: |
| result = {'deleted': 0, 'failed': 0, 'freed': 0, 'errors': []} |
| self.finished.emit(result) |
|
|
|
|
| |
| |
| |
|
|
| class DiskPanel(QWidget): |
| def __init__(self): |
| super().__init__() |
| layout = QVBoxLayout(self) |
| layout.setContentsMargins(10, 10, 10, 10) |
| |
| title = QLabel("Disk Usage Overview") |
| title.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold)) |
| title.setStyleSheet("color: #e94560; border: none;") |
| layout.addWidget(title) |
| |
| self.table = QTableWidget() |
| self.table.setColumnCount(5) |
| self.table.setHorizontalHeaderLabels(["Drive", "Total", "Used", "Free", "Usage %"]) |
| self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) |
| self.table.verticalHeader().setVisible(False) |
| self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) |
| self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) |
| layout.addWidget(self.table) |
| |
| self.summary = QLabel("Click 'Refresh' to scan disks") |
| self.summary.setStyleSheet("color: #aaa; border: none; padding: 5px;") |
| layout.addWidget(self.summary) |
| |
| btn_layout = QHBoxLayout() |
| self.refresh_btn = QPushButton("Refresh Disks") |
| self.refresh_btn.setObjectName("primary") |
| self.refresh_btn.clicked.connect(self.refresh) |
| btn_layout.addWidget(self.refresh_btn) |
| btn_layout.addStretch() |
| layout.addLayout(btn_layout) |
| |
| self.refresh() |
| |
| def refresh(self): |
| disks = DiskScanner.get_disks() |
| self.table.setRowCount(len(disks)) |
| |
| for i, d in enumerate(disks): |
| self.table.setItem(i, 0, QTableWidgetItem(d.drive)) |
| self.table.setItem(i, 1, QTableWidgetItem(format_size(d.total))) |
| self.table.setItem(i, 2, QTableWidgetItem(format_size(d.used))) |
| self.table.setItem(i, 3, QTableWidgetItem(format_size(d.free))) |
| |
| bar = QProgressBar() |
| bar.setValue(int(d.percent)) |
| bar.setFormat(f"{d.percent}%") |
| if d.percent > 90: |
| bar.setStyleSheet("QProgressBar::chunk { background-color: #ff3333; border-radius: 5px; }") |
| elif d.percent > 70: |
| bar.setStyleSheet("QProgressBar::chunk { background-color: #ffaa00; border-radius: 5px; }") |
| self.table.setCellWidget(i, 4, bar) |
| |
| total = sum(d.total for d in disks) |
| free = sum(d.free for d in disks) |
| self.summary.setText(f"Total Storage: {format_size(total)} | Free: {format_size(free)}") |
|
|
|
|
| |
| |
| |
|
|
| class FileScannerPanel(QWidget): |
| log_signal = pyqtSignal(str) |
| |
| def __init__(self): |
| super().__init__() |
| layout = QVBoxLayout(self) |
| layout.setContentsMargins(10, 10, 10, 10) |
| |
| title = QLabel("Large File Scanner") |
| title.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold)) |
| title.setStyleSheet("color: #e94560; border: none;") |
| layout.addWidget(title) |
| |
| |
| ctrl_layout = QHBoxLayout() |
| |
| self.path_input = QLineEdit() |
| self.path_input.setPlaceholderText("Directory to scan...") |
| self.path_input.setText(str(Path.home())) |
| ctrl_layout.addWidget(self.path_input) |
| |
| browse_btn = QPushButton("Browse") |
| browse_btn.clicked.connect(self._browse) |
| ctrl_layout.addWidget(browse_btn) |
| |
| self.threshold_spin = QSpinBox() |
| self.threshold_spin.setRange(1, 10000) |
| self.threshold_spin.setValue(50) |
| self.threshold_spin.setSuffix(" MB") |
| ctrl_layout.addWidget(QLabel("Min size:")) |
| ctrl_layout.addWidget(self.threshold_spin) |
| |
| layout.addLayout(ctrl_layout) |
| |
| |
| self.tree = QTreeWidget() |
| self.tree.setHeaderLabels(["File", "Size", "Category", "Modified", "Path"]) |
| self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) |
| self.tree.setColumnWidth(1, 100) |
| self.tree.setColumnWidth(2, 80) |
| self.tree.setColumnWidth(3, 120) |
| self.tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) |
| self.tree.setAlternatingRowColors(True) |
| layout.addWidget(self.tree) |
| |
| |
| status_layout = QHBoxLayout() |
| self.status_label = QLabel("Ready") |
| self.status_label.setStyleSheet("color: #aaa; border: none;") |
| status_layout.addWidget(self.status_label) |
| status_layout.addStretch() |
| |
| self.scan_btn = QPushButton("Scan") |
| self.scan_btn.setObjectName("primary") |
| self.scan_btn.clicked.connect(self._start_scan) |
| status_layout.addWidget(self.scan_btn) |
| |
| self.delete_btn = QPushButton("Delete Selected") |
| self.delete_btn.setObjectName("danger") |
| self.delete_btn.clicked.connect(self._delete_selected) |
| self.delete_btn.setEnabled(False) |
| status_layout.addWidget(self.delete_btn) |
| |
| layout.addLayout(status_layout) |
| |
| self._worker = None |
| |
| def _browse(self): |
| path = QFileDialog.getExistingDirectory(self, "Select Directory") |
| if path: |
| self.path_input.setText(path) |
| |
| def _start_scan(self): |
| path = self.path_input.text() |
| if not os.path.exists(path): |
| QMessageBox.warning(self, "Error", "Path does not exist") |
| return |
| |
| self.tree.clear() |
| self.scan_btn.setEnabled(False) |
| self.status_label.setText("Scanning...") |
| |
| threshold = self.threshold_spin.value() * 1024 * 1024 |
| self._worker = ScanWorker(path, threshold) |
| self._worker.finished.connect(self._on_scan_done) |
| self._worker.status.connect(lambda s: self.status_label.setText(s)) |
| self._worker.start() |
| |
| def _on_scan_done(self, results: list): |
| self.scan_btn.setEnabled(True) |
| self.delete_btn.setEnabled(len(results) > 0) |
| |
| total_size = 0 |
| for entry in results: |
| item = QTreeWidgetItem() |
| item.setText(0, os.path.basename(entry.path)) |
| item.setText(1, format_size(entry.size)) |
| item.setText(2, entry.category) |
| item.setText(3, entry.modified) |
| item.setText(4, entry.path) |
| item.setData(0, Qt.ItemDataRole.UserRole, entry.path) |
| |
| |
| colors = { |
| 'video': '#ff6b6b', 'audio': '#ffa500', 'image': '#00d4aa', |
| 'archive': '#6c63ff', 'installer': '#ff00aa', 'document': '#00bfff', |
| } |
| color = colors.get(entry.category, '#888888') |
| item.setForeground(2, QColor(color)) |
| |
| self.tree.addTopLevelItem(item) |
| total_size += entry.size |
| |
| self.status_label.setText(f"Found {len(results)} files ({format_size(total_size)} total)") |
| self.log_signal.emit(f"Scan complete: {len(results)} large files found ({format_size(total_size)})") |
| |
| def _delete_selected(self): |
| items = self.tree.selectedItems() |
| if not items: |
| QMessageBox.information(self, "Info", "Select files to delete first") |
| return |
| |
| paths = [item.data(0, Qt.ItemDataRole.UserRole) for item in items] |
| |
| reply = QMessageBox.question( |
| self, "Confirm Delete", |
| f"Permanently delete {len(paths)} file(s)?\n\nThis cannot be undone!", |
| QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No |
| ) |
| if reply == QMessageBox.StandardButton.Yes: |
| result = DiskCleaner.delete_files(paths) |
| QMessageBox.information( |
| self, "Done", |
| f"Deleted: {result['deleted']}\nFailed: {result['failed']}\nFreed: {format_size(result['freed'])}" |
| ) |
| self.log_signal.emit(f"Deleted {result['deleted']} files, freed {format_size(result['freed'])}") |
| self._start_scan() |
|
|
|
|
| |
| |
| |
|
|
| class CleaningPanel(QWidget): |
| log_signal = pyqtSignal(str) |
| |
| def __init__(self): |
| super().__init__() |
| layout = QVBoxLayout(self) |
| layout.setContentsMargins(10, 10, 10, 10) |
| |
| title = QLabel("System Cleaner") |
| title.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold)) |
| title.setStyleSheet("color: #e94560; border: none;") |
| layout.addWidget(title) |
| |
| |
| info_frame = QFrame() |
| info_frame.setStyleSheet("background-color: #16213e; border: 1px solid #0f3460; border-radius: 8px; padding: 10px;") |
| info_layout = QVBoxLayout(info_frame) |
| self.temp_info = QLabel("Calculating temp files...") |
| self.temp_info.setStyleSheet("color: #e0e0e0; border: none; font-size: 13px;") |
| info_layout.addWidget(self.temp_info) |
| layout.addWidget(info_frame) |
| |
| |
| opts_frame = QGroupBox("Cleaning Options") |
| opts_layout = QVBoxLayout(opts_frame) |
| |
| self.chk_temp = QCheckBox("Temporary Files (User & System Temp)") |
| self.chk_temp.setChecked(True) |
| opts_layout.addWidget(self.chk_temp) |
| |
| self.chk_recycle = QCheckBox("Recycle Bin / Trash") |
| self.chk_recycle.setChecked(True) |
| opts_layout.addWidget(self.chk_recycle) |
| |
| self.chk_cache = QCheckBox("Browser Cache (AppData/Cache)") |
| self.chk_cache.setChecked(False) |
| opts_layout.addWidget(self.chk_cache) |
| |
| self.chk_logs = QCheckBox("Old Log Files (.log)") |
| self.chk_logs.setChecked(False) |
| opts_layout.addWidget(self.chk_logs) |
| |
| self.chk_empty = QCheckBox("Empty Directories") |
| self.chk_empty.setChecked(False) |
| opts_layout.addWidget(self.chk_empty) |
| |
| layout.addWidget(opts_frame) |
| |
| |
| self.progress = QProgressBar() |
| self.progress.setValue(0) |
| self.progress.setFormat("Ready") |
| layout.addWidget(self.progress) |
| |
| self.result_label = QLabel("") |
| self.result_label.setStyleSheet("color: #00d4aa; border: none; font-weight: bold;") |
| layout.addWidget(self.result_label) |
| |
| |
| btn_layout = QHBoxLayout() |
| |
| self.analyze_btn = QPushButton("Analyze") |
| self.analyze_btn.setObjectName("primary") |
| self.analyze_btn.clicked.connect(self._analyze) |
| btn_layout.addWidget(self.analyze_btn) |
| |
| self.clean_btn = QPushButton("Clean Now") |
| self.clean_btn.setObjectName("danger") |
| self.clean_btn.clicked.connect(self._clean) |
| btn_layout.addWidget(self.clean_btn) |
| |
| btn_layout.addStretch() |
| layout.addLayout(btn_layout) |
| layout.addStretch() |
| |
| |
| QTimer.singleShot(500, self._analyze) |
| |
| def _analyze(self): |
| self.temp_info.setText("Analyzing...") |
| size, count = DiskScanner.scan_temp_size() |
| self.temp_info.setText( |
| f"Temp Files: {format_size(size)} ({count:,} files)\n" |
| f"Locations: {', '.join(os.path.basename(p) for p in DiskScanner.get_temp_paths()[:3])}" |
| ) |
| |
| def _clean(self): |
| reply = QMessageBox.question( |
| self, "Confirm", |
| "Clean selected items? Files will be permanently deleted.", |
| QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No |
| ) |
| if reply != QMessageBox.StandardButton.Yes: |
| return |
| |
| self.clean_btn.setEnabled(False) |
| self.progress.setFormat("Cleaning... %p%") |
| self.progress.setValue(10) |
| |
| total_freed = 0 |
| total_deleted = 0 |
| |
| if self.chk_temp.isChecked(): |
| self.progress.setValue(30) |
| result = DiskCleaner.clean_temp_files() |
| total_freed += result['freed'] |
| total_deleted += result['deleted'] |
| |
| if self.chk_recycle.isChecked(): |
| self.progress.setValue(60) |
| DiskCleaner.empty_recycle_bin() |
| |
| if self.chk_empty.isChecked(): |
| self.progress.setValue(80) |
| DiskCleaner.clean_empty_dirs(str(Path.home())) |
| |
| self.progress.setValue(100) |
| self.progress.setFormat("Done!") |
| self.clean_btn.setEnabled(True) |
| |
| self.result_label.setText(f"Cleaned {total_deleted:,} files | Freed {format_size(total_freed)}") |
| self.log_signal.emit(f"Cleaning complete: {total_deleted} files deleted, {format_size(total_freed)} freed") |
| |
| QTimer.singleShot(1000, self._analyze) |
|
|
|
|
| |
| |
| |
|
|
| class LogPanel(QWidget): |
| def __init__(self): |
| super().__init__() |
| layout = QVBoxLayout(self) |
| layout.setContentsMargins(10, 10, 10, 10) |
| |
| title = QLabel("Activity Log") |
| title.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold)) |
| title.setStyleSheet("color: #e94560; border: none;") |
| layout.addWidget(title) |
| |
| self.log_text = QTextEdit() |
| self.log_text.setReadOnly(True) |
| self.log_text.setFont(QFont("Consolas", 10)) |
| layout.addWidget(self.log_text) |
| |
| btn_layout = QHBoxLayout() |
| clear_btn = QPushButton("Clear Log") |
| clear_btn.clicked.connect(self.log_text.clear) |
| btn_layout.addWidget(clear_btn) |
| btn_layout.addStretch() |
| layout.addLayout(btn_layout) |
| |
| self.add_log("Storage Cleaner initialized") |
| |
| def add_log(self, message: str): |
| timestamp = datetime.now().strftime("%H:%M:%S") |
| self.log_text.append(f"[{timestamp}] {message}") |
|
|
|
|
| |
| |
| |
|
|
| class StorageCleanerWindow(QMainWindow): |
| def __init__(self): |
| super().__init__() |
| self.setWindowTitle("Storage Cleaner") |
| self.setMinimumSize(1000, 700) |
| self.resize(1200, 800) |
| |
| self._setup_menu() |
| self._setup_ui() |
| self._setup_statusbar() |
| |
| def _setup_menu(self): |
| menubar = self.menuBar() |
| |
| file_menu = menubar.addMenu("File") |
| |
| refresh_action = QAction("Refresh Disks", self) |
| refresh_action.setShortcut("F5") |
| refresh_action.triggered.connect(lambda: self.disk_panel.refresh()) |
| file_menu.addAction(refresh_action) |
| |
| file_menu.addSeparator() |
| |
| exit_action = QAction("Exit", self) |
| exit_action.setShortcut("Ctrl+Q") |
| exit_action.triggered.connect(self.close) |
| file_menu.addAction(exit_action) |
| |
| tools_menu = menubar.addMenu("Tools") |
| |
| scan_action = QAction("Scan Large Files", self) |
| scan_action.setShortcut("Ctrl+S") |
| scan_action.triggered.connect(lambda: self.tabs.setCurrentIndex(1)) |
| tools_menu.addAction(scan_action) |
| |
| clean_action = QAction("Clean System", self) |
| clean_action.setShortcut("Ctrl+D") |
| clean_action.triggered.connect(lambda: self.tabs.setCurrentIndex(2)) |
| tools_menu.addAction(clean_action) |
| |
| help_menu = menubar.addMenu("Help") |
| about_action = QAction("About", self) |
| about_action.triggered.connect(self._show_about) |
| help_menu.addAction(about_action) |
| |
| def _setup_ui(self): |
| central = QWidget() |
| self.setCentralWidget(central) |
| layout = QVBoxLayout(central) |
| layout.setContentsMargins(0, 0, 0, 0) |
| |
| self.tabs = QTabWidget() |
| |
| |
| self.disk_panel = DiskPanel() |
| self.tabs.addTab(self.disk_panel, "Disks") |
| |
| |
| self.scanner_panel = FileScannerPanel() |
| self.tabs.addTab(self.scanner_panel, "Large Files") |
| |
| |
| self.cleaning_panel = CleaningPanel() |
| self.tabs.addTab(self.cleaning_panel, "Clean") |
| |
| |
| self.log_panel = LogPanel() |
| self.tabs.addTab(self.log_panel, "Log") |
| |
| layout.addWidget(self.tabs) |
| |
| |
| self.scanner_panel.log_signal.connect(self.log_panel.add_log) |
| self.cleaning_panel.log_signal.connect(self.log_panel.add_log) |
| |
| def _setup_statusbar(self): |
| self.statusBar().showMessage("Ready") |
| |
| |
| self._timer = QTimer(self) |
| self._timer.timeout.connect(self._update_status) |
| self._timer.start(5000) |
| self._update_status() |
| |
| def _update_status(self): |
| disks = DiskScanner.get_disks() |
| if disks: |
| free = sum(d.free for d in disks) |
| self.statusBar().showMessage(f"Free space: {format_size(free)} | {platform.system()} {platform.release()}") |
| |
| def _show_about(self): |
| QMessageBox.about( |
| self, "About Storage Cleaner", |
| "Storage Cleaner v1.0\n\n" |
| "A professional disk cleanup utility.\n" |
| "Built with PyQt6.\n\n" |
| "Features:\n" |
| "- Disk usage overview\n" |
| "- Large file scanner\n" |
| "- Temp file cleaner\n" |
| "- Recycle bin cleaner\n" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| DARK_QSS = """ |
| QMainWindow { background-color: #1a1a2e; color: #e0e0e0; } |
| QMenuBar { background-color: #16213e; color: #e0e0e0; border-bottom: 1px solid #0f3460; } |
| QMenuBar::item { padding: 5px 15px; } |
| QMenuBar::item:selected { background-color: #0f3460; } |
| QMenu { background-color: #16213e; border: 1px solid #0f3460; color: #e0e0e0; } |
| QMenu::item { padding: 5px 30px 5px 20px; } |
| QMenu::item:selected { background-color: #0f3460; border: 1px solid #e94560; } |
| QLabel { color: #e0e0e0; } |
| QGroupBox { font-weight: bold; border: 1px solid #0f3460; border-radius: 5px; margin-top: 10px; padding-top: 10px; color: #e0e0e0; } |
| QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; } |
| QProgressBar { border: 1px solid #0f3460; border-radius: 5px; text-align: center; background-color: #16213e; color: #e0e0e0; height: 20px; } |
| QProgressBar::chunk { background-color: #e94560; border-radius: 5px; } |
| QPushButton { background-color: #0f3460; border: 1px solid #e94560; border-radius: 5px; padding: 8px 18px; color: #e0e0e0; font-weight: bold; } |
| QPushButton:hover { background-color: #e94560; } |
| QPushButton:pressed { background-color: #c72c41; } |
| QPushButton:disabled { background-color: #16213e; border-color: #0f3460; color: #555; } |
| QPushButton#primary { background-color: #e94560; border: 1px solid #c72c41; color: #fff; } |
| QPushButton#primary:hover { background-color: #c72c41; } |
| QPushButton#danger { background-color: #c72c41; border: 1px solid #e94560; color: #fff; } |
| QPushButton#danger:hover { background-color: #e94560; } |
| QTreeWidget, QTableWidget { background-color: #16213e; border: 1px solid #0f3460; color: #e0e0e0; alternate-background-color: #1a1a2e; } |
| QTreeWidget::item:hover, QTableWidget::item:hover { background-color: #0f3460; } |
| QTreeWidget::item:selected, QTableWidget::item:selected { background-color: #0f3460; border: 1px solid #e94560; } |
| QHeaderView::section { background-color: #0f3460; color: #e0e0e0; padding: 5px; border: 1px solid #0f3460; font-weight: bold; } |
| QTextEdit { background-color: #16213e; border: 1px solid #0f3460; color: #e0e0e0; } |
| QLineEdit { background-color: #16213e; border: 1px solid #0f3460; border-radius: 5px; padding: 5px; color: #e0e0e0; } |
| QLineEdit:focus { border-color: #e94560; } |
| QTabWidget::pane { border: 1px solid #0f3460; } |
| QTabBar::tab { background-color: #16213e; color: #e0e0e0; padding: 8px 20px; border: 1px solid #0f3460; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; } |
| QTabBar::tab:selected { background-color: #0f3460; color: #e94560; } |
| QScrollBar:vertical { background-color: #16213e; width: 10px; } |
| QScrollBar::handle:vertical { background-color: #0f3460; min-height: 20px; border-radius: 5px; } |
| QScrollBar::handle:vertical:hover { background-color: #e94560; } |
| QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; } |
| QCheckBox { color: #e0e0e0; spacing: 5px; } |
| QCheckBox::indicator { width: 18px; height: 18px; border: 1px solid #0f3460; border-radius: 4px; background-color: #16213e; } |
| QCheckBox::indicator:checked { background-color: #e94560; border-color: #c72c41; } |
| QSpinBox { background-color: #16213e; border: 1px solid #0f3460; border-radius: 5px; padding: 5px; color: #e0e0e0; } |
| QComboBox { background-color: #16213e; border: 1px solid #0f3460; border-radius: 5px; padding: 5px; color: #e0e0e0; } |
| QStatusBar { background-color: #16213e; color: #e0e0e0; border-top: 1px solid #0f3460; } |
| QFrame { background-color: #16213e; border: 1px solid #0f3460; border-radius: 5px; } |
| QToolTip { background-color: #16213e; color: #e0e0e0; border: 1px solid #e94560; padding: 5px; } |
| QMessageBox { background-color: #1a1a2e; } |
| """ |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| app = QApplication(sys.argv) |
| app.setStyle("Fusion") |
| app.setStyleSheet(DARK_QSS) |
| |
| font = QFont("Segoe UI", 10) |
| app.setFont(font) |
| |
| window = StorageCleanerWindow() |
| window.show() |
| |
| sys.exit(app.exec()) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|