#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 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 # ============================================================================ # CORE - SCANNER # ============================================================================ @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 # 50MB 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 # Check /home if separate 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): # Skip system/hidden dirs 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 # ============================================================================ # CORE - CLEANER # ============================================================================ 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 # ============================================================================ # WORKER THREADS # ============================================================================ 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) # ============================================================================ # WIDGETS - DISK PANEL # ============================================================================ 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)}") # ============================================================================ # WIDGETS - FILE SCANNER PANEL # ============================================================================ 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) # Controls 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) # Tree 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 + buttons 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) # Color by category 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() # Refresh # ============================================================================ # WIDGETS - CLEANING PANEL # ============================================================================ 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) # Temp info 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) # Options 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) # Progress 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) # Buttons 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() # Initial analysis 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) # ============================================================================ # WIDGETS - LOG PANEL # ============================================================================ 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}") # ============================================================================ # MAIN WINDOW # ============================================================================ 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() # Tab 1: Disk Overview self.disk_panel = DiskPanel() self.tabs.addTab(self.disk_panel, "Disks") # Tab 2: File Scanner self.scanner_panel = FileScannerPanel() self.tabs.addTab(self.scanner_panel, "Large Files") # Tab 3: Cleaner self.cleaning_panel = CleaningPanel() self.tabs.addTab(self.cleaning_panel, "Clean") # Tab 4: Log self.log_panel = LogPanel() self.tabs.addTab(self.log_panel, "Log") layout.addWidget(self.tabs) # Connect log signals 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") # Live clock 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 THEME # ============================================================================ 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; } """ # ============================================================================ # ENTRY POINT # ============================================================================ 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()