MoneyPack-Security-Suite / storage_cleaner.py
MoneyPack's picture
Add PyQt6 Storage Cleaner utility
1e74b38 verified
#!/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()