MoneyPack commited on
Commit
11a5007
·
verified ·
1 Parent(s): 937c3f6

MoneyPackCleaner v1.0 - Premium cleaner with bunny branding, all 7 pro features

Browse files
Files changed (1) hide show
  1. moneypack_cleaner.py +1714 -0
moneypack_cleaner.py ADDED
@@ -0,0 +1,1714 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ MoneyPackCleaner v1.0
5
+ Premium Storage Optimization Suite
6
+ Created by MoneyPack
7
+
8
+ Features:
9
+ - Disk analysis with donut chart visualization
10
+ - Large file scanner with category detection
11
+ - Duplicate file finder
12
+ - Folder size analyzer
13
+ - System cleaner (temp, cache, trash, thumbnails, browser)
14
+ - File age scanner (find forgotten files)
15
+ - Startup manager
16
+ - Scheduled auto-cleaning
17
+ - System tray with quick actions
18
+ - Toast notifications
19
+ - Dry run mode
20
+ - Splash screen with branding
21
+ """
22
+
23
+ import os
24
+ import sys
25
+ import time
26
+ import shutil
27
+ import hashlib
28
+ import platform
29
+ import subprocess
30
+ import json
31
+ import math
32
+ from pathlib import Path
33
+ from datetime import datetime, timedelta
34
+ from dataclasses import dataclass, field
35
+ from typing import List, Tuple, Dict
36
+ from collections import defaultdict
37
+
38
+ from PyQt6.QtWidgets import (
39
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
40
+ QTabWidget, QLabel, QPushButton, QProgressBar, QCheckBox,
41
+ QTreeWidget, QTreeWidgetItem, QTableWidget, QTableWidgetItem,
42
+ QHeaderView, QAbstractItemView, QTextEdit, QLineEdit,
43
+ QFileDialog, QMessageBox, QStatusBar, QGroupBox, QFrame,
44
+ QComboBox, QSpinBox, QMenu, QSystemTrayIcon, QSplashScreen,
45
+ QGraphicsOpacityEffect, QSizePolicy, QScrollArea, QRadioButton
46
+ )
47
+ from PyQt6.QtCore import (
48
+ Qt, QThread, pyqtSignal, QTimer, QSize, QPropertyAnimation,
49
+ QPoint, QRect, QEasingCurve, QSequentialAnimationGroup
50
+ )
51
+ from PyQt6.QtGui import (
52
+ QFont, QColor, QAction, QIcon, QPainter, QPen, QBrush,
53
+ QLinearGradient, QRadialGradient, QPixmap, QPainterPath,
54
+ QConicalGradient, QFontMetrics
55
+ )
56
+
57
+
58
+ # ============================================================================
59
+ # BRANDING COLORS
60
+ # ============================================================================
61
+
62
+ class Brand:
63
+ # MoneyPack signature palette - dark luxury
64
+ BG_DARK = "#0a0a14"
65
+ BG_PANEL = "#12121f"
66
+ BG_CARD = "#1a1a2e"
67
+ BORDER = "#2a2a40"
68
+
69
+ # Accent colors
70
+ GOLD = "#d4af37" # Premium gold
71
+ GOLD_LIGHT = "#f0d060"
72
+ PINK = "#ff2d75" # Playboy-inspired hot pink
73
+ PINK_LIGHT = "#ff5c8a"
74
+ CYAN = "#00e5ff"
75
+ PURPLE = "#9c27b0"
76
+
77
+ # Text
78
+ TEXT = "#e8e8f0"
79
+ TEXT_DIM = "#7a7a90"
80
+ TEXT_MUTED = "#4a4a60"
81
+
82
+ # Status
83
+ SUCCESS = "#00e676"
84
+ WARNING = "#ffab00"
85
+ DANGER = "#ff1744"
86
+
87
+
88
+ # ============================================================================
89
+ # UTILITY
90
+ # ============================================================================
91
+
92
+ def format_size(size_bytes: int) -> str:
93
+ if size_bytes <= 0:
94
+ return "0 B"
95
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
96
+ if size_bytes < 1024:
97
+ return f"{size_bytes:.1f} {unit}"
98
+ size_bytes /= 1024
99
+ return f"{size_bytes:.1f} PB"
100
+
101
+
102
+ def safe_remove(path: str) -> bool:
103
+ try:
104
+ if os.path.isfile(path):
105
+ os.remove(path)
106
+ elif os.path.isdir(path):
107
+ shutil.rmtree(path)
108
+ return True
109
+ except (OSError, PermissionError):
110
+ return False
111
+
112
+
113
+ def days_since_access(path: str) -> int:
114
+ try:
115
+ atime = os.path.getatime(path)
116
+ return (datetime.now() - datetime.fromtimestamp(atime)).days
117
+ except (OSError, ValueError):
118
+ return -1
119
+
120
+
121
+ # ============================================================================
122
+ # DATA CLASSES
123
+ # ============================================================================
124
+
125
+ @dataclass
126
+ class DiskInfo:
127
+ drive: str
128
+ mount_point: str
129
+ total: int
130
+ used: int
131
+ free: int
132
+ percent: float
133
+
134
+
135
+ @dataclass
136
+ class FileEntry:
137
+ path: str
138
+ size: int
139
+ modified: str
140
+ ext: str = ""
141
+ category: str = ""
142
+ days_old: int = 0
143
+
144
+
145
+ @dataclass
146
+ class DuplicateGroup:
147
+ size: int
148
+ files: List[str] = field(default_factory=list)
149
+
150
+
151
+ @dataclass
152
+ class FolderEntry:
153
+ path: str
154
+ size: int
155
+ file_count: int
156
+ percent: float = 0.0
157
+
158
+
159
+ # ============================================================================
160
+ # CORE ENGINE
161
+ # ============================================================================
162
+
163
+ class DiskScanner:
164
+ SKIP_DIRS = {
165
+ '$Recycle.Bin', 'System Volume Information', 'Windows', 'WinSxS',
166
+ 'node_modules', '.git', '__pycache__', 'venv', '.venv',
167
+ 'Recovery', 'PerfLogs', '.Trash', 'proc', 'sys', 'dev',
168
+ }
169
+
170
+ CATEGORY_MAP = {
171
+ 'Video': {'.mp4','.avi','.mkv','.mov','.flv','.wmv','.webm','.m4v','.mpg'},
172
+ 'Audio': {'.mp3','.wav','.flac','.aac','.ogg','.wma','.m4a','.opus'},
173
+ 'Image': {'.jpg','.jpeg','.png','.gif','.bmp','.tiff','.webp','.svg','.psd','.raw'},
174
+ 'Archive': {'.zip','.rar','.7z','.tar','.gz','.bz2','.xz','.iso','.cab'},
175
+ 'Document': {'.pdf','.doc','.docx','.xls','.xlsx','.ppt','.pptx','.txt','.odt'},
176
+ 'Installer': {'.exe','.msi','.dmg','.deb','.rpm','.appimage','.apk'},
177
+ 'Database': {'.db','.sqlite','.mdf','.sql','.bak'},
178
+ 'Virtual': {'.vmdk','.vdi','.vhd','.vhdx','.ova'},
179
+ }
180
+
181
+ @classmethod
182
+ def get_disks(cls) -> List[DiskInfo]:
183
+ disks = []
184
+ if sys.platform == 'win32':
185
+ try:
186
+ import ctypes
187
+ bitmask = ctypes.windll.kernel32.GetLogicalDrives()
188
+ for letter in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
189
+ if bitmask & 1:
190
+ drive = f"{letter}:\\"
191
+ try:
192
+ u = shutil.disk_usage(drive)
193
+ if u.total > 0:
194
+ disks.append(DiskInfo(f"{letter}:", drive, u.total, u.used, u.free, round(u.used/u.total*100, 1)))
195
+ except (OSError, PermissionError):
196
+ pass
197
+ bitmask >>= 1
198
+ except Exception:
199
+ pass
200
+ else:
201
+ seen = set()
202
+ for mount in ['/', str(Path.home())]:
203
+ try:
204
+ u = shutil.disk_usage(mount)
205
+ if u.total > 0 and u.total not in seen:
206
+ seen.add(u.total)
207
+ disks.append(DiskInfo(mount, mount, u.total, u.used, u.free, round(u.used/u.total*100, 1)))
208
+ except (OSError, PermissionError):
209
+ pass
210
+ return disks
211
+
212
+ @classmethod
213
+ def get_category(cls, ext: str) -> str:
214
+ ext = ext.lower()
215
+ for cat, exts in cls.CATEGORY_MAP.items():
216
+ if ext in exts:
217
+ return cat
218
+ return "Other"
219
+
220
+ @classmethod
221
+ def scan_large_files(cls, root, threshold=50*1024*1024, max_files=500, callback=None):
222
+ results = []
223
+ count = 0
224
+ for dirpath, dirnames, filenames in os.walk(root):
225
+ dirnames[:] = [d for d in dirnames if d not in cls.SKIP_DIRS and not d.startswith('.')]
226
+ for fname in filenames:
227
+ try:
228
+ fpath = os.path.join(dirpath, fname)
229
+ size = os.path.getsize(fpath)
230
+ if size >= threshold:
231
+ ext = os.path.splitext(fname)[1].lower()
232
+ try:
233
+ mtime = datetime.fromtimestamp(os.path.getmtime(fpath)).strftime('%Y-%m-%d')
234
+ except:
235
+ mtime = "?"
236
+ results.append(FileEntry(fpath, size, mtime, ext, cls.get_category(ext)))
237
+ count += 1
238
+ if callback and count % 20 == 0:
239
+ callback(count)
240
+ if count >= max_files:
241
+ return sorted(results, key=lambda x: x.size, reverse=True)
242
+ except (OSError, PermissionError):
243
+ continue
244
+ return sorted(results, key=lambda x: x.size, reverse=True)
245
+
246
+ @classmethod
247
+ def scan_old_files(cls, root, min_days=180, min_size=1024*1024, max_files=300, callback=None):
248
+ """Find files not accessed in min_days."""
249
+ results = []
250
+ count = 0
251
+ for dirpath, dirnames, filenames in os.walk(root):
252
+ dirnames[:] = [d for d in dirnames if d not in cls.SKIP_DIRS and not d.startswith('.')]
253
+ for fname in filenames:
254
+ try:
255
+ fpath = os.path.join(dirpath, fname)
256
+ size = os.path.getsize(fpath)
257
+ if size >= min_size:
258
+ days = days_since_access(fpath)
259
+ if days >= min_days:
260
+ ext = os.path.splitext(fname)[1].lower()
261
+ results.append(FileEntry(fpath, size, f"{days}d ago", ext, cls.get_category(ext), days))
262
+ count += 1
263
+ if callback and count % 20 == 0:
264
+ callback(count)
265
+ if count >= max_files:
266
+ return sorted(results, key=lambda x: x.days_old, reverse=True)
267
+ except (OSError, PermissionError):
268
+ continue
269
+ return sorted(results, key=lambda x: x.days_old, reverse=True)
270
+
271
+ @classmethod
272
+ def find_duplicates(cls, root, min_size=1024*1024, callback=None):
273
+ size_map = defaultdict(list)
274
+ count = 0
275
+ for dirpath, dirnames, filenames in os.walk(root):
276
+ dirnames[:] = [d for d in dirnames if d not in cls.SKIP_DIRS and not d.startswith('.')]
277
+ for fname in filenames:
278
+ try:
279
+ fpath = os.path.join(dirpath, fname)
280
+ size = os.path.getsize(fpath)
281
+ if size >= min_size:
282
+ size_map[size].append(fpath)
283
+ count += 1
284
+ if callback and count % 200 == 0:
285
+ callback(f"Indexing: {count}")
286
+ except (OSError, PermissionError):
287
+ continue
288
+
289
+ duplicates = []
290
+ hash_map = defaultdict(list)
291
+ candidates = [(s, ps) for s, ps in size_map.items() if len(ps) > 1]
292
+
293
+ for size, paths in candidates:
294
+ for fpath in paths:
295
+ try:
296
+ h = hashlib.md5()
297
+ with open(fpath, 'rb') as f:
298
+ h.update(f.read(8192))
299
+ if size > 8192:
300
+ f.seek(-8192, 2)
301
+ h.update(f.read(8192))
302
+ hash_map[f"{size}_{h.hexdigest()}"].append(fpath)
303
+ except (OSError, PermissionError):
304
+ continue
305
+
306
+ for key, paths in hash_map.items():
307
+ if len(paths) > 1:
308
+ size = int(key.split('_')[0])
309
+ duplicates.append(DuplicateGroup(size, paths))
310
+
311
+ return sorted(duplicates, key=lambda x: x.size * len(x.files), reverse=True)
312
+
313
+ @classmethod
314
+ def analyze_folders(cls, root, callback=None):
315
+ folders = []
316
+ try:
317
+ for entry in sorted(os.scandir(root), key=lambda e: e.name):
318
+ if entry.is_dir() and entry.name not in cls.SKIP_DIRS and not entry.name.startswith('.'):
319
+ try:
320
+ size = 0
321
+ fcount = 0
322
+ for r, d, fs in os.walk(entry.path):
323
+ d[:] = [x for x in d if x not in cls.SKIP_DIRS]
324
+ for f in fs:
325
+ try:
326
+ size += os.path.getsize(os.path.join(r, f))
327
+ fcount += 1
328
+ except:
329
+ pass
330
+ if size > 0:
331
+ folders.append(FolderEntry(entry.path, size, fcount))
332
+ if callback:
333
+ callback(len(folders))
334
+ except (OSError, PermissionError):
335
+ pass
336
+ except (OSError, PermissionError):
337
+ pass
338
+
339
+ total = sum(f.size for f in folders)
340
+ for f in folders:
341
+ f.percent = round(f.size / total * 100, 1) if total > 0 else 0
342
+ return sorted(folders, key=lambda x: x.size, reverse=True)
343
+
344
+ @classmethod
345
+ def get_temp_paths(cls):
346
+ paths = []
347
+ if sys.platform == 'win32':
348
+ candidates = [
349
+ os.environ.get('TEMP', ''),
350
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Temp'),
351
+ os.path.join(os.environ.get('WINDIR', r'C:\Windows'), 'Temp'),
352
+ os.path.join(os.environ.get('WINDIR', r'C:\Windows'), 'Prefetch'),
353
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Microsoft', 'Windows', 'INetCache'),
354
+ ]
355
+ else:
356
+ candidates = ['/tmp', '/var/tmp', os.path.join(str(Path.home()), '.cache'),
357
+ os.path.join(str(Path.home()), '.local', 'share', 'Trash')]
358
+ return [p for p in candidates if p and os.path.exists(p)]
359
+
360
+ @classmethod
361
+ def scan_temp_size(cls):
362
+ total, count = 0, 0
363
+ for d in cls.get_temp_paths():
364
+ try:
365
+ for r, _, fs in os.walk(d):
366
+ for f in fs:
367
+ try:
368
+ total += os.path.getsize(os.path.join(r, f))
369
+ count += 1
370
+ except:
371
+ pass
372
+ except:
373
+ pass
374
+ return total, count
375
+
376
+ @classmethod
377
+ def get_startup_items(cls):
378
+ """Get startup programs (Windows)."""
379
+ items = []
380
+ if sys.platform == 'win32':
381
+ # Registry Run keys
382
+ for key_path in [
383
+ r"HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
384
+ r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
385
+ ]:
386
+ try:
387
+ r = subprocess.run(["reg", "query", key_path], capture_output=True, text=True, timeout=5)
388
+ if r.returncode == 0:
389
+ for line in r.stdout.split("\n"):
390
+ if "REG_SZ" in line or "REG_EXPAND_SZ" in line:
391
+ parts = line.strip().split(None, 2)
392
+ if len(parts) >= 3:
393
+ name = parts[0]
394
+ value = parts[2] if len(parts) > 2 else ""
395
+ items.append({"name": name, "command": value, "source": key_path, "enabled": True})
396
+ except:
397
+ pass
398
+
399
+ # Startup folder
400
+ startup = Path.home() / "AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup"
401
+ if startup.exists():
402
+ for f in startup.iterdir():
403
+ items.append({"name": f.name, "command": str(f), "source": "Startup Folder", "enabled": True})
404
+ else:
405
+ # Linux autostart
406
+ autostart = Path.home() / ".config" / "autostart"
407
+ if autostart.exists():
408
+ for f in autostart.iterdir():
409
+ if f.suffix == ".desktop":
410
+ items.append({"name": f.stem, "command": str(f), "source": "autostart", "enabled": True})
411
+ return items
412
+
413
+
414
+ class DiskCleaner:
415
+ @classmethod
416
+ def delete_files(cls, paths):
417
+ result = {'deleted': 0, 'failed': 0, 'freed': 0}
418
+ for p in paths:
419
+ try:
420
+ if not os.path.exists(p):
421
+ continue
422
+ size = os.path.getsize(p) if os.path.isfile(p) else 0
423
+ if safe_remove(p):
424
+ result['deleted'] += 1
425
+ result['freed'] += size
426
+ else:
427
+ result['failed'] += 1
428
+ except:
429
+ result['failed'] += 1
430
+ return result
431
+
432
+ @classmethod
433
+ def clean_temp(cls, callback=None):
434
+ result = {'deleted': 0, 'freed': 0}
435
+ for d in DiskScanner.get_temp_paths():
436
+ try:
437
+ for r, _, fs in os.walk(d):
438
+ for f in fs:
439
+ fp = os.path.join(r, f)
440
+ try:
441
+ size = os.path.getsize(fp)
442
+ os.remove(fp)
443
+ result['deleted'] += 1
444
+ result['freed'] += size
445
+ if callback and result['deleted'] % 50 == 0:
446
+ callback(result['deleted'])
447
+ except:
448
+ pass
449
+ except:
450
+ pass
451
+ return result
452
+
453
+ @classmethod
454
+ def empty_trash(cls):
455
+ if sys.platform == 'win32':
456
+ try:
457
+ import ctypes
458
+ ctypes.windll.shell32.SHEmptyRecycleBinW(None, None, 0x07)
459
+ return True
460
+ except:
461
+ return False
462
+ else:
463
+ for trash in [str(Path.home() / ".local/share/Trash"), str(Path.home() / ".Trash")]:
464
+ if os.path.exists(trash):
465
+ try:
466
+ shutil.rmtree(trash)
467
+ os.makedirs(trash, exist_ok=True)
468
+ return True
469
+ except:
470
+ pass
471
+ return False
472
+
473
+ @classmethod
474
+ def clean_browser_cache(cls):
475
+ result = {'deleted': 0, 'freed': 0}
476
+ dirs = []
477
+ if sys.platform == 'win32':
478
+ local = os.environ.get('LOCALAPPDATA', '')
479
+ dirs = [
480
+ os.path.join(local, 'Google', 'Chrome', 'User Data', 'Default', 'Cache'),
481
+ os.path.join(local, 'Google', 'Chrome', 'User Data', 'Default', 'Code Cache'),
482
+ os.path.join(local, 'Microsoft', 'Edge', 'User Data', 'Default', 'Cache'),
483
+ ]
484
+ else:
485
+ home = str(Path.home())
486
+ dirs = [os.path.join(home, '.cache', 'google-chrome'), os.path.join(home, '.cache', 'mozilla')]
487
+
488
+ for d in dirs:
489
+ if os.path.exists(d):
490
+ for r, _, fs in os.walk(d):
491
+ for f in fs:
492
+ fp = os.path.join(r, f)
493
+ try:
494
+ size = os.path.getsize(fp)
495
+ os.remove(fp)
496
+ result['deleted'] += 1
497
+ result['freed'] += size
498
+ except:
499
+ pass
500
+ return result
501
+
502
+
503
+ # ============================================================================
504
+ # WORKER THREADS
505
+ # ============================================================================
506
+
507
+ class ScanWorker(QThread):
508
+ progress = pyqtSignal(int)
509
+ finished = pyqtSignal(list)
510
+ status = pyqtSignal(str)
511
+
512
+ def __init__(self, mode, root, **kwargs):
513
+ super().__init__()
514
+ self.mode = mode
515
+ self.root = root
516
+ self.kwargs = kwargs
517
+
518
+ def run(self):
519
+ self.status.emit(f"Scanning {os.path.basename(self.root)}...")
520
+ if self.mode == 'large':
521
+ r = DiskScanner.scan_large_files(self.root, self.kwargs.get('threshold', 50*1024*1024), callback=lambda c: self.progress.emit(c))
522
+ elif self.mode == 'old':
523
+ r = DiskScanner.scan_old_files(self.root, self.kwargs.get('min_days', 180), callback=lambda c: self.progress.emit(c))
524
+ elif self.mode == 'duplicates':
525
+ r = DiskScanner.find_duplicates(self.root, self.kwargs.get('min_size', 1024*1024), callback=lambda s: self.status.emit(s))
526
+ elif self.mode == 'folders':
527
+ r = DiskScanner.analyze_folders(self.root, callback=lambda c: self.progress.emit(c))
528
+ else:
529
+ r = []
530
+ self.finished.emit(r)
531
+
532
+
533
+ class CleanWorker(QThread):
534
+ finished = pyqtSignal(dict)
535
+ status = pyqtSignal(str)
536
+
537
+ def __init__(self, tasks):
538
+ super().__init__()
539
+ self.tasks = tasks
540
+
541
+ def run(self):
542
+ result = {'deleted': 0, 'freed': 0}
543
+ if 'temp' in self.tasks:
544
+ self.status.emit("Cleaning temp files...")
545
+ r = DiskCleaner.clean_temp()
546
+ result['deleted'] += r['deleted']
547
+ result['freed'] += r['freed']
548
+ if 'trash' in self.tasks:
549
+ self.status.emit("Emptying trash...")
550
+ DiskCleaner.empty_trash()
551
+ if 'browser' in self.tasks:
552
+ self.status.emit("Cleaning browser cache...")
553
+ r = DiskCleaner.clean_browser_cache()
554
+ result['deleted'] += r['deleted']
555
+ result['freed'] += r['freed']
556
+ self.finished.emit(result)
557
+
558
+
559
+ # ============================================================================
560
+ # CUSTOM WIDGETS
561
+ # ============================================================================
562
+
563
+ class BunnyLogo(QWidget):
564
+ """Custom painted bunny logo widget - MoneyPack branded."""
565
+
566
+ def __init__(self, size=80):
567
+ super().__init__()
568
+ self.setFixedSize(size, size)
569
+ self._size = size
570
+
571
+ def paintEvent(self, event):
572
+ p = QPainter(self)
573
+ p.setRenderHint(QPainter.RenderHint.Antialiasing)
574
+
575
+ s = self._size
576
+ cx, cy = s/2, s/2
577
+
578
+ # Glow behind
579
+ glow = QRadialGradient(cx, cy, s*0.45)
580
+ glow.setColorAt(0, QColor(Brand.PINK + "40"))
581
+ glow.setColorAt(1, QColor(Brand.PINK + "00"))
582
+ p.setBrush(QBrush(glow))
583
+ p.setPen(Qt.PenStyle.NoPen)
584
+ p.drawEllipse(int(cx - s*0.4), int(cy - s*0.4), int(s*0.8), int(s*0.8))
585
+
586
+ # Bunny head (circle)
587
+ head_y = cy + s*0.08
588
+ head_r = s * 0.22
589
+ p.setBrush(QColor(Brand.PINK))
590
+ p.setPen(QPen(QColor(Brand.GOLD), 2))
591
+ p.drawEllipse(int(cx - head_r), int(head_y - head_r), int(head_r*2), int(head_r*2))
592
+
593
+ # Ears
594
+ ear_w = s * 0.08
595
+ ear_h = s * 0.28
596
+ ear_y = head_y - head_r - ear_h * 0.6
597
+
598
+ # Left ear
599
+ ear_path = QPainterPath()
600
+ ear_path.addEllipse(cx - s*0.12 - ear_w/2, ear_y, ear_w, ear_h)
601
+ p.setBrush(QColor(Brand.PINK))
602
+ p.drawPath(ear_path)
603
+
604
+ # Right ear
605
+ ear_path2 = QPainterPath()
606
+ ear_path2.addEllipse(cx + s*0.12 - ear_w/2, ear_y, ear_w, ear_h)
607
+ p.drawPath(ear_path2)
608
+
609
+ # Inner ears (gold)
610
+ p.setBrush(QColor(Brand.GOLD))
611
+ p.setPen(Qt.PenStyle.NoPen)
612
+ inner_w = ear_w * 0.5
613
+ inner_h = ear_h * 0.6
614
+ p.drawEllipse(int(cx - s*0.12 - inner_w/2), int(ear_y + ear_h*0.2), int(inner_w), int(inner_h))
615
+ p.drawEllipse(int(cx + s*0.12 - inner_w/2), int(ear_y + ear_h*0.2), int(inner_w), int(inner_h))
616
+
617
+ # Eyes
618
+ p.setBrush(QColor("#ffffff"))
619
+ eye_size = s * 0.04
620
+ p.drawEllipse(int(cx - s*0.08), int(head_y - s*0.03), int(eye_size), int(eye_size))
621
+ p.drawEllipse(int(cx + s*0.05), int(head_y - s*0.03), int(eye_size), int(eye_size))
622
+
623
+ # Bow tie (gold)
624
+ p.setBrush(QColor(Brand.GOLD))
625
+ p.setPen(Qt.PenStyle.NoPen)
626
+ bow_y = head_y + head_r - s*0.02
627
+ bow_path = QPainterPath()
628
+ bow_path.moveTo(cx, bow_y)
629
+ bow_path.lineTo(cx - s*0.1, bow_y - s*0.05)
630
+ bow_path.lineTo(cx - s*0.1, bow_y + s*0.05)
631
+ bow_path.closeSubpath()
632
+ bow_path.moveTo(cx, bow_y)
633
+ bow_path.lineTo(cx + s*0.1, bow_y - s*0.05)
634
+ bow_path.lineTo(cx + s*0.1, bow_y + s*0.05)
635
+ bow_path.closeSubpath()
636
+ p.drawPath(bow_path)
637
+
638
+ p.end()
639
+
640
+
641
+ class DonutChart(QWidget):
642
+ """Animated donut chart showing disk usage breakdown."""
643
+
644
+ def __init__(self, size=200):
645
+ super().__init__()
646
+ self.setFixedSize(size, size)
647
+ self._size = size
648
+ self._segments = [] # [(percent, color, label)]
649
+ self._center_text = ""
650
+ self._center_sub = ""
651
+
652
+ def set_data(self, segments, center_text="", center_sub=""):
653
+ self._segments = segments
654
+ self._center_text = center_text
655
+ self._center_sub = center_sub
656
+ self.update()
657
+
658
+ def paintEvent(self, event):
659
+ p = QPainter(self)
660
+ p.setRenderHint(QPainter.RenderHint.Antialiasing)
661
+
662
+ s = self._size
663
+ margin = 10
664
+ outer_r = (s - margin*2) / 2
665
+ inner_r = outer_r * 0.65
666
+ cx, cy = s/2, s/2
667
+
668
+ # Background ring
669
+ p.setPen(Qt.PenStyle.NoPen)
670
+ p.setBrush(QColor(Brand.BORDER))
671
+ p.drawEllipse(int(cx - outer_r), int(cy - outer_r), int(outer_r*2), int(outer_r*2))
672
+
673
+ # Inner circle (dark)
674
+ p.setBrush(QColor(Brand.BG_PANEL))
675
+ p.drawEllipse(int(cx - inner_r), int(cy - inner_r), int(inner_r*2), int(inner_r*2))
676
+
677
+ # Draw segments
678
+ if self._segments:
679
+ rect = QRect(int(cx - outer_r), int(cy - outer_r), int(outer_r*2), int(outer_r*2))
680
+ start_angle = 90 * 16 # Start from top
681
+
682
+ for percent, color, label in self._segments:
683
+ span = int(percent / 100 * 360 * 16)
684
+ if span == 0:
685
+ continue
686
+ p.setBrush(QColor(color))
687
+ p.setPen(QPen(QColor(Brand.BG_PANEL), 2))
688
+ p.drawPie(rect, start_angle, -span)
689
+ start_angle -= span
690
+
691
+ # Redraw inner circle
692
+ p.setPen(Qt.PenStyle.NoPen)
693
+ p.setBrush(QColor(Brand.BG_PANEL))
694
+ p.drawEllipse(int(cx - inner_r), int(cy - inner_r), int(inner_r*2), int(inner_r*2))
695
+
696
+ # Center text
697
+ if self._center_text:
698
+ p.setPen(QColor(Brand.TEXT))
699
+ font = QFont("Segoe UI", int(s * 0.08), QFont.Weight.Bold)
700
+ p.setFont(font)
701
+ p.drawText(QRect(0, int(cy - s*0.1), s, int(s*0.15)), Qt.AlignmentFlag.AlignCenter, self._center_text)
702
+
703
+ if self._center_sub:
704
+ p.setPen(QColor(Brand.TEXT_DIM))
705
+ font = QFont("Segoe UI", int(s * 0.05))
706
+ p.setFont(font)
707
+ p.drawText(QRect(0, int(cy + s*0.02), s, int(s*0.12)), Qt.AlignmentFlag.AlignCenter, self._center_sub)
708
+
709
+ p.end()
710
+
711
+
712
+ class ToastNotification(QFrame):
713
+ """Slide-in toast notification."""
714
+
715
+ def __init__(self, parent, message, toast_type="info", duration=3000):
716
+ super().__init__(parent)
717
+ self.setFixedSize(350, 60)
718
+
719
+ colors = {
720
+ "info": Brand.CYAN,
721
+ "success": Brand.SUCCESS,
722
+ "warning": Brand.WARNING,
723
+ "error": Brand.DANGER,
724
+ }
725
+ color = colors.get(toast_type, Brand.CYAN)
726
+
727
+ self.setStyleSheet(f"""
728
+ QFrame {{
729
+ background-color: {Brand.BG_CARD};
730
+ border: 1px solid {color};
731
+ border-left: 4px solid {color};
732
+ border-radius: 8px;
733
+ }}
734
+ """)
735
+
736
+ layout = QHBoxLayout(self)
737
+ layout.setContentsMargins(12, 8, 12, 8)
738
+
739
+ icons = {"info": "i", "success": "+", "warning": "!", "error": "X"}
740
+ icon_lbl = QLabel(icons.get(toast_type, "i"))
741
+ icon_lbl.setStyleSheet(f"color: {color}; font-size: 18px; font-weight: bold; border: none;")
742
+ layout.addWidget(icon_lbl)
743
+
744
+ msg_lbl = QLabel(message)
745
+ msg_lbl.setStyleSheet(f"color: {Brand.TEXT}; font-size: 12px; border: none;")
746
+ msg_lbl.setWordWrap(True)
747
+ layout.addWidget(msg_lbl)
748
+
749
+ # Position at top-right of parent
750
+ parent_rect = parent.rect()
751
+ self.move(parent_rect.width() - 370, 10)
752
+ self.show()
753
+
754
+ # Slide in animation
755
+ self._anim = QPropertyAnimation(self, b"pos")
756
+ self._anim.setDuration(300)
757
+ self._anim.setStartValue(QPoint(parent_rect.width(), 10))
758
+ self._anim.setEndValue(QPoint(parent_rect.width() - 370, 10))
759
+ self._anim.setEasingCurve(QEasingCurve.Type.OutCubic)
760
+ self._anim.start()
761
+
762
+ # Auto dismiss
763
+ QTimer.singleShot(duration, self._dismiss)
764
+
765
+ def _dismiss(self):
766
+ self._fade = QPropertyAnimation(self, b"pos")
767
+ self._fade.setDuration(200)
768
+ self._fade.setEndValue(QPoint(self.parent().rect().width(), self.y()))
769
+ self._fade.setEasingCurve(QEasingCurve.Type.InCubic)
770
+ self._fade.finished.connect(self.deleteLater)
771
+ self._fade.start()
772
+
773
+
774
+ # ============================================================================
775
+ # TAB WIDGETS
776
+ # ============================================================================
777
+
778
+ class DashboardTab(QWidget):
779
+ log_signal = pyqtSignal(str)
780
+
781
+ def __init__(self):
782
+ super().__init__()
783
+ layout = QVBoxLayout(self)
784
+ layout.setContentsMargins(16, 16, 16, 16)
785
+ layout.setSpacing(16)
786
+
787
+ # Header with logo
788
+ header = QHBoxLayout()
789
+ self.logo = BunnyLogo(60)
790
+ header.addWidget(self.logo)
791
+
792
+ title_layout = QVBoxLayout()
793
+ title = QLabel("MoneyPackCleaner")
794
+ title.setFont(QFont("Segoe UI", 22, QFont.Weight.Bold))
795
+ title.setStyleSheet(f"color: {Brand.GOLD}; border: none;")
796
+ title_layout.addWidget(title)
797
+ subtitle = QLabel("Premium Storage Optimization")
798
+ subtitle.setStyleSheet(f"color: {Brand.TEXT_DIM}; border: none; font-size: 12px;")
799
+ title_layout.addWidget(subtitle)
800
+ header.addLayout(title_layout)
801
+ header.addStretch()
802
+
803
+ # Quick actions
804
+ quick_scan_btn = QPushButton("Quick Clean")
805
+ quick_scan_btn.setObjectName("primary")
806
+ quick_scan_btn.setFixedHeight(36)
807
+ quick_scan_btn.clicked.connect(self._quick_clean)
808
+ header.addWidget(quick_scan_btn)
809
+
810
+ layout.addLayout(header)
811
+
812
+ # Main content: Chart + Stats
813
+ content = QHBoxLayout()
814
+
815
+ # Donut chart
816
+ chart_frame = QFrame()
817
+ chart_frame.setStyleSheet(f"background-color: {Brand.BG_PANEL}; border: 1px solid {Brand.BORDER}; border-radius: 12px;")
818
+ chart_layout = QVBoxLayout(chart_frame)
819
+ chart_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
820
+ self.chart = DonutChart(220)
821
+ chart_layout.addWidget(self.chart, alignment=Qt.AlignmentFlag.AlignCenter)
822
+ content.addWidget(chart_frame)
823
+
824
+ # Stats cards
825
+ stats_layout = QVBoxLayout()
826
+ stats_layout.setSpacing(10)
827
+
828
+ self.cards = {}
829
+ card_data = [
830
+ ("total", "Total Storage", Brand.GOLD),
831
+ ("used", "Used Space", Brand.PINK),
832
+ ("free", "Free Space", Brand.SUCCESS),
833
+ ("temp", "Cleanable", Brand.WARNING),
834
+ ]
835
+ for key, label, color in card_data:
836
+ card = self._make_card(label, "---", color)
837
+ self.cards[key] = card
838
+ stats_layout.addWidget(card)
839
+
840
+ stats_layout.addStretch()
841
+ content.addLayout(stats_layout)
842
+ layout.addLayout(content)
843
+
844
+ # Disk table
845
+ self.table = QTableWidget()
846
+ self.table.setColumnCount(5)
847
+ self.table.setHorizontalHeaderLabels(["Drive", "Total", "Used", "Free", "Usage"])
848
+ self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
849
+ self.table.verticalHeader().setVisible(False)
850
+ self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
851
+ self.table.setMaximumHeight(150)
852
+ layout.addWidget(self.table)
853
+
854
+ self.refresh()
855
+
856
+ def _make_card(self, label, value, color):
857
+ frame = QFrame()
858
+ frame.setFixedHeight(65)
859
+ frame.setStyleSheet(f"background-color: {Brand.BG_PANEL}; border: 1px solid {color}33; border-radius: 10px;")
860
+ fl = QHBoxLayout(frame)
861
+ fl.setContentsMargins(14, 8, 14, 8)
862
+
863
+ dot = QLabel("*")
864
+ dot.setStyleSheet(f"color: {color}; font-size: 24px; border: none;")
865
+ fl.addWidget(dot)
866
+
867
+ text_l = QVBoxLayout()
868
+ text_l.setSpacing(0)
869
+ lbl = QLabel(label)
870
+ lbl.setStyleSheet(f"color: {Brand.TEXT_DIM}; font-size: 11px; border: none;")
871
+ text_l.addWidget(lbl)
872
+ val = QLabel(value)
873
+ val.setObjectName("card_val")
874
+ val.setStyleSheet(f"color: {color}; font-size: 16px; font-weight: bold; border: none;")
875
+ text_l.addWidget(val)
876
+ fl.addLayout(text_l)
877
+ fl.addStretch()
878
+ return frame
879
+
880
+ def _update_card(self, key, value):
881
+ card = self.cards[key]
882
+ lbl = card.findChild(QLabel, "card_val")
883
+ if lbl:
884
+ lbl.setText(value)
885
+
886
+ def refresh(self):
887
+ disks = DiskScanner.get_disks()
888
+ self.table.setRowCount(len(disks))
889
+
890
+ for i, d in enumerate(disks):
891
+ self.table.setItem(i, 0, QTableWidgetItem(d.drive))
892
+ self.table.setItem(i, 1, QTableWidgetItem(format_size(d.total)))
893
+ self.table.setItem(i, 2, QTableWidgetItem(format_size(d.used)))
894
+ self.table.setItem(i, 3, QTableWidgetItem(format_size(d.free)))
895
+ bar = QProgressBar()
896
+ bar.setValue(int(d.percent))
897
+ bar.setFormat(f"{d.percent}%")
898
+ self.table.setCellWidget(i, 4, bar)
899
+
900
+ total = sum(d.total for d in disks)
901
+ used = sum(d.used for d in disks)
902
+ free = sum(d.free for d in disks)
903
+
904
+ self._update_card("total", format_size(total))
905
+ self._update_card("used", format_size(used))
906
+ self._update_card("free", format_size(free))
907
+
908
+ temp_size, _ = DiskScanner.scan_temp_size()
909
+ self._update_card("temp", format_size(temp_size))
910
+
911
+ # Update donut
912
+ if total > 0:
913
+ used_pct = used / total * 100
914
+ free_pct = free / total * 100
915
+ self.chart.set_data(
916
+ [(used_pct, Brand.PINK, "Used"), (free_pct, Brand.SUCCESS, "Free")],
917
+ center_text=f"{used_pct:.0f}%",
918
+ center_sub="Used"
919
+ )
920
+
921
+ def _quick_clean(self):
922
+ self.log_signal.emit("Quick clean started...")
923
+ r = DiskCleaner.clean_temp()
924
+ self.log_signal.emit(f"Quick clean: {r['deleted']} files, {format_size(r['freed'])} freed")
925
+ self.refresh()
926
+ # Toast
927
+ ToastNotification(self.window(), f"Freed {format_size(r['freed'])}", "success")
928
+
929
+
930
+ class LargeFilesTab(QWidget):
931
+ log_signal = pyqtSignal(str)
932
+
933
+ def __init__(self):
934
+ super().__init__()
935
+ layout = QVBoxLayout(self)
936
+ layout.setContentsMargins(12, 12, 12, 12)
937
+ layout.setSpacing(10)
938
+
939
+ title = QLabel("Large File Scanner")
940
+ title.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold))
941
+ title.setStyleSheet(f"color: {Brand.GOLD}; border: none;")
942
+ layout.addWidget(title)
943
+
944
+ ctrl = QHBoxLayout()
945
+ self.path_input = QLineEdit(str(Path.home()))
946
+ ctrl.addWidget(self.path_input)
947
+ browse_btn = QPushButton("Browse")
948
+ browse_btn.clicked.connect(lambda: self._browse())
949
+ ctrl.addWidget(browse_btn)
950
+ ctrl.addWidget(QLabel("Min:"))
951
+ self.threshold = QSpinBox()
952
+ self.threshold.setRange(1, 10000)
953
+ self.threshold.setValue(50)
954
+ self.threshold.setSuffix(" MB")
955
+ ctrl.addWidget(self.threshold)
956
+
957
+ self.dry_run_chk = QCheckBox("Dry Run (preview only)")
958
+ ctrl.addWidget(self.dry_run_chk)
959
+ layout.addLayout(ctrl)
960
+
961
+ self.tree = QTreeWidget()
962
+ self.tree.setHeaderLabels(["File", "Size", "Category", "Modified", "Path"])
963
+ self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
964
+ self.tree.setColumnWidth(1, 90)
965
+ self.tree.setColumnWidth(2, 80)
966
+ self.tree.setColumnWidth(3, 90)
967
+ self.tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
968
+ self.tree.setAlternatingRowColors(True)
969
+ layout.addWidget(self.tree)
970
+
971
+ bottom = QHBoxLayout()
972
+ self.status_lbl = QLabel("Ready")
973
+ self.status_lbl.setStyleSheet(f"color: {Brand.TEXT_DIM}; border: none;")
974
+ bottom.addWidget(self.status_lbl)
975
+ bottom.addStretch()
976
+ self.scan_btn = QPushButton("Scan")
977
+ self.scan_btn.setObjectName("primary")
978
+ self.scan_btn.clicked.connect(self._scan)
979
+ bottom.addWidget(self.scan_btn)
980
+ self.del_btn = QPushButton("Delete Selected")
981
+ self.del_btn.setObjectName("danger")
982
+ self.del_btn.clicked.connect(self._delete)
983
+ self.del_btn.setEnabled(False)
984
+ bottom.addWidget(self.del_btn)
985
+ layout.addLayout(bottom)
986
+
987
+ self._worker = None
988
+
989
+ def _browse(self):
990
+ p = QFileDialog.getExistingDirectory(self, "Select")
991
+ if p:
992
+ self.path_input.setText(p)
993
+
994
+ def _scan(self):
995
+ self.tree.clear()
996
+ self.scan_btn.setEnabled(False)
997
+ self.status_lbl.setText("Scanning...")
998
+ self._worker = ScanWorker('large', self.path_input.text(), threshold=self.threshold.value()*1024*1024)
999
+ self._worker.finished.connect(self._done)
1000
+ self._worker.status.connect(lambda s: self.status_lbl.setText(s))
1001
+ self._worker.start()
1002
+
1003
+ def _done(self, results):
1004
+ self.scan_btn.setEnabled(True)
1005
+ self.del_btn.setEnabled(len(results) > 0)
1006
+ colors = {'Video':'#ff6b6b','Audio':'#ffa500','Image':'#00d4aa','Archive':'#6c63ff','Installer':'#ff00aa','Document':'#00bfff'}
1007
+ total = sum(e.size for e in results)
1008
+ for e in results:
1009
+ item = QTreeWidgetItem()
1010
+ item.setText(0, os.path.basename(e.path))
1011
+ item.setText(1, format_size(e.size))
1012
+ item.setText(2, e.category)
1013
+ item.setText(3, e.modified)
1014
+ item.setText(4, e.path)
1015
+ item.setData(0, Qt.ItemDataRole.UserRole, e.path)
1016
+ item.setForeground(2, QColor(colors.get(e.category, '#888')))
1017
+ self.tree.addTopLevelItem(item)
1018
+ self.status_lbl.setText(f"{len(results)} files ({format_size(total)})")
1019
+ self.log_signal.emit(f"Large files: {len(results)} found, {format_size(total)}")
1020
+
1021
+ def _delete(self):
1022
+ items = self.tree.selectedItems()
1023
+ if not items:
1024
+ return
1025
+ paths = [i.data(0, Qt.ItemDataRole.UserRole) for i in items]
1026
+
1027
+ if self.dry_run_chk.isChecked():
1028
+ total = sum(os.path.getsize(p) for p in paths if os.path.exists(p))
1029
+ QMessageBox.information(self, "Dry Run", f"Would delete {len(paths)} files\nWould free: {format_size(total)}")
1030
+ return
1031
+
1032
+ if QMessageBox.question(self, "Delete", f"Permanently delete {len(paths)} files?") == QMessageBox.StandardButton.Yes:
1033
+ r = DiskCleaner.delete_files(paths)
1034
+ self.log_signal.emit(f"Deleted {r['deleted']} files, freed {format_size(r['freed'])}")
1035
+ ToastNotification(self.window(), f"Freed {format_size(r['freed'])}", "success")
1036
+ self._scan()
1037
+
1038
+
1039
+ class OldFilesTab(QWidget):
1040
+ log_signal = pyqtSignal(str)
1041
+
1042
+ def __init__(self):
1043
+ super().__init__()
1044
+ layout = QVBoxLayout(self)
1045
+ layout.setContentsMargins(12, 12, 12, 12)
1046
+ layout.setSpacing(10)
1047
+
1048
+ title = QLabel("Forgotten Files")
1049
+ title.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold))
1050
+ title.setStyleSheet(f"color: {Brand.GOLD}; border: none;")
1051
+ layout.addWidget(title)
1052
+
1053
+ desc = QLabel("Find files you haven't opened in months or years")
1054
+ desc.setStyleSheet(f"color: {Brand.TEXT_DIM}; border: none;")
1055
+ layout.addWidget(desc)
1056
+
1057
+ ctrl = QHBoxLayout()
1058
+ self.path_input = QLineEdit(str(Path.home()))
1059
+ ctrl.addWidget(self.path_input)
1060
+ browse_btn = QPushButton("Browse")
1061
+ browse_btn.clicked.connect(lambda: self._browse())
1062
+ ctrl.addWidget(browse_btn)
1063
+ ctrl.addWidget(QLabel("Not accessed in:"))
1064
+ self.days_spin = QSpinBox()
1065
+ self.days_spin.setRange(30, 3650)
1066
+ self.days_spin.setValue(180)
1067
+ self.days_spin.setSuffix(" days")
1068
+ ctrl.addWidget(self.days_spin)
1069
+ layout.addLayout(ctrl)
1070
+
1071
+ self.tree = QTreeWidget()
1072
+ self.tree.setHeaderLabels(["File", "Size", "Last Access", "Category", "Path"])
1073
+ self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
1074
+ self.tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
1075
+ self.tree.setAlternatingRowColors(True)
1076
+ layout.addWidget(self.tree)
1077
+
1078
+ bottom = QHBoxLayout()
1079
+ self.status_lbl = QLabel("Find files gathering dust on your drive")
1080
+ self.status_lbl.setStyleSheet(f"color: {Brand.TEXT_DIM}; border: none;")
1081
+ bottom.addWidget(self.status_lbl)
1082
+ bottom.addStretch()
1083
+ self.scan_btn = QPushButton("Find Old Files")
1084
+ self.scan_btn.setObjectName("primary")
1085
+ self.scan_btn.clicked.connect(self._scan)
1086
+ bottom.addWidget(self.scan_btn)
1087
+ self.del_btn = QPushButton("Delete Selected")
1088
+ self.del_btn.setObjectName("danger")
1089
+ self.del_btn.clicked.connect(self._delete)
1090
+ self.del_btn.setEnabled(False)
1091
+ bottom.addWidget(self.del_btn)
1092
+ layout.addLayout(bottom)
1093
+ self._worker = None
1094
+
1095
+ def _browse(self):
1096
+ p = QFileDialog.getExistingDirectory(self, "Select")
1097
+ if p:
1098
+ self.path_input.setText(p)
1099
+
1100
+ def _scan(self):
1101
+ self.tree.clear()
1102
+ self.scan_btn.setEnabled(False)
1103
+ self._worker = ScanWorker('old', self.path_input.text(), min_days=self.days_spin.value())
1104
+ self._worker.finished.connect(self._done)
1105
+ self._worker.status.connect(lambda s: self.status_lbl.setText(s))
1106
+ self._worker.start()
1107
+
1108
+ def _done(self, results):
1109
+ self.scan_btn.setEnabled(True)
1110
+ self.del_btn.setEnabled(len(results) > 0)
1111
+ total = sum(e.size for e in results)
1112
+ for e in results:
1113
+ item = QTreeWidgetItem()
1114
+ item.setText(0, os.path.basename(e.path))
1115
+ item.setText(1, format_size(e.size))
1116
+ item.setText(2, e.modified)
1117
+ item.setText(3, e.category)
1118
+ item.setText(4, e.path)
1119
+ item.setData(0, Qt.ItemDataRole.UserRole, e.path)
1120
+ if e.days_old > 365:
1121
+ item.setForeground(2, QColor(Brand.DANGER))
1122
+ elif e.days_old > 180:
1123
+ item.setForeground(2, QColor(Brand.WARNING))
1124
+ self.tree.addTopLevelItem(item)
1125
+ self.status_lbl.setText(f"{len(results)} forgotten files ({format_size(total)})")
1126
+ self.log_signal.emit(f"Old files: {len(results)} found, {format_size(total)}")
1127
+
1128
+ def _delete(self):
1129
+ items = self.tree.selectedItems()
1130
+ if not items:
1131
+ return
1132
+ paths = [i.data(0, Qt.ItemDataRole.UserRole) for i in items]
1133
+ if QMessageBox.question(self, "Delete", f"Delete {len(paths)} old files?") == QMessageBox.StandardButton.Yes:
1134
+ r = DiskCleaner.delete_files(paths)
1135
+ self.log_signal.emit(f"Deleted {r['deleted']} old files")
1136
+ ToastNotification(self.window(), f"Freed {format_size(r['freed'])}", "success")
1137
+ self._scan()
1138
+
1139
+
1140
+ class DuplicatesTab(QWidget):
1141
+ log_signal = pyqtSignal(str)
1142
+
1143
+ def __init__(self):
1144
+ super().__init__()
1145
+ layout = QVBoxLayout(self)
1146
+ layout.setContentsMargins(12, 12, 12, 12)
1147
+ layout.setSpacing(10)
1148
+
1149
+ title = QLabel("Duplicate Finder")
1150
+ title.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold))
1151
+ title.setStyleSheet(f"color: {Brand.GOLD}; border: none;")
1152
+ layout.addWidget(title)
1153
+
1154
+ ctrl = QHBoxLayout()
1155
+ self.path_input = QLineEdit(str(Path.home()))
1156
+ ctrl.addWidget(self.path_input)
1157
+ browse_btn = QPushButton("Browse")
1158
+ browse_btn.clicked.connect(lambda: self._browse())
1159
+ ctrl.addWidget(browse_btn)
1160
+ ctrl.addWidget(QLabel("Min:"))
1161
+ self.min_spin = QSpinBox()
1162
+ self.min_spin.setRange(1, 1000)
1163
+ self.min_spin.setValue(1)
1164
+ self.min_spin.setSuffix(" MB")
1165
+ ctrl.addWidget(self.min_spin)
1166
+ layout.addLayout(ctrl)
1167
+
1168
+ self.tree = QTreeWidget()
1169
+ self.tree.setHeaderLabels(["File", "Size", "Path"])
1170
+ self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
1171
+ self.tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
1172
+ self.tree.setAlternatingRowColors(True)
1173
+ layout.addWidget(self.tree)
1174
+
1175
+ bottom = QHBoxLayout()
1176
+ self.status_lbl = QLabel("Ready")
1177
+ self.status_lbl.setStyleSheet(f"color: {Brand.TEXT_DIM}; border: none;")
1178
+ bottom.addWidget(self.status_lbl)
1179
+ bottom.addStretch()
1180
+ self.scan_btn = QPushButton("Find Duplicates")
1181
+ self.scan_btn.setObjectName("primary")
1182
+ self.scan_btn.clicked.connect(self._scan)
1183
+ bottom.addWidget(self.scan_btn)
1184
+ self.del_btn = QPushButton("Delete Selected")
1185
+ self.del_btn.setObjectName("danger")
1186
+ self.del_btn.clicked.connect(self._delete)
1187
+ self.del_btn.setEnabled(False)
1188
+ bottom.addWidget(self.del_btn)
1189
+ layout.addLayout(bottom)
1190
+ self._worker = None
1191
+
1192
+ def _browse(self):
1193
+ p = QFileDialog.getExistingDirectory(self, "Select")
1194
+ if p:
1195
+ self.path_input.setText(p)
1196
+
1197
+ def _scan(self):
1198
+ self.tree.clear()
1199
+ self.scan_btn.setEnabled(False)
1200
+ self.status_lbl.setText("Scanning...")
1201
+ self._worker = ScanWorker('duplicates', self.path_input.text(), min_size=self.min_spin.value()*1024*1024)
1202
+ self._worker.finished.connect(self._done)
1203
+ self._worker.status.connect(lambda s: self.status_lbl.setText(s))
1204
+ self._worker.start()
1205
+
1206
+ def _done(self, groups):
1207
+ self.scan_btn.setEnabled(True)
1208
+ self.del_btn.setEnabled(len(groups) > 0)
1209
+ waste = 0
1210
+ for g in groups[:80]:
1211
+ w = g.size * (len(g.files) - 1)
1212
+ waste += w
1213
+ parent = QTreeWidgetItem()
1214
+ parent.setText(0, f"[{len(g.files)} copies] {os.path.basename(g.files[0])}")
1215
+ parent.setText(1, format_size(g.size))
1216
+ parent.setForeground(0, QColor(Brand.WARNING))
1217
+ for fp in g.files:
1218
+ child = QTreeWidgetItem(parent)
1219
+ child.setText(0, os.path.basename(fp))
1220
+ child.setText(1, format_size(g.size))
1221
+ child.setText(2, fp)
1222
+ child.setData(0, Qt.ItemDataRole.UserRole, fp)
1223
+ self.tree.addTopLevelItem(parent)
1224
+ self.status_lbl.setText(f"{len(groups)} groups | Wasted: {format_size(waste)}")
1225
+ self.log_signal.emit(f"Duplicates: {len(groups)} groups, {format_size(waste)} wasted")
1226
+
1227
+ def _delete(self):
1228
+ items = self.tree.selectedItems()
1229
+ paths = [i.data(0, Qt.ItemDataRole.UserRole) for i in items if i.data(0, Qt.ItemDataRole.UserRole)]
1230
+ if paths and QMessageBox.question(self, "Delete", f"Delete {len(paths)} copies?") == QMessageBox.StandardButton.Yes:
1231
+ r = DiskCleaner.delete_files(paths)
1232
+ ToastNotification(self.window(), f"Freed {format_size(r['freed'])}", "success")
1233
+ self._scan()
1234
+
1235
+
1236
+ class CleanerTab(QWidget):
1237
+ log_signal = pyqtSignal(str)
1238
+
1239
+ def __init__(self):
1240
+ super().__init__()
1241
+ layout = QVBoxLayout(self)
1242
+ layout.setContentsMargins(12, 12, 12, 12)
1243
+ layout.setSpacing(12)
1244
+
1245
+ title = QLabel("System Cleaner")
1246
+ title.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold))
1247
+ title.setStyleSheet(f"color: {Brand.GOLD}; border: none;")
1248
+ layout.addWidget(title)
1249
+
1250
+ # Info
1251
+ self.info_lbl = QLabel("Analyzing...")
1252
+ self.info_lbl.setStyleSheet(f"color: {Brand.TEXT}; border: none; font-size: 13px; padding: 8px;")
1253
+ layout.addWidget(self.info_lbl)
1254
+
1255
+ # Options
1256
+ opts = QGroupBox("Select what to clean")
1257
+ ol = QVBoxLayout(opts)
1258
+ self.chk_temp = QCheckBox("Temporary Files")
1259
+ self.chk_temp.setChecked(True)
1260
+ ol.addWidget(self.chk_temp)
1261
+ self.chk_trash = QCheckBox("Recycle Bin / Trash")
1262
+ self.chk_trash.setChecked(True)
1263
+ ol.addWidget(self.chk_trash)
1264
+ self.chk_browser = QCheckBox("Browser Cache (Chrome, Edge, Firefox)")
1265
+ ol.addWidget(self.chk_browser)
1266
+ self.chk_thumbs = QCheckBox("Thumbnail Cache")
1267
+ self.chk_thumbs.setChecked(True)
1268
+ ol.addWidget(self.chk_thumbs)
1269
+ layout.addWidget(opts)
1270
+
1271
+ # Dry run
1272
+ self.dry_run = QCheckBox("Dry Run - show what would be cleaned without deleting")
1273
+ self.dry_run.setStyleSheet(f"color: {Brand.WARNING};")
1274
+ layout.addWidget(self.dry_run)
1275
+
1276
+ self.progress = QProgressBar()
1277
+ self.progress.setValue(0)
1278
+ self.progress.setFormat("Ready")
1279
+ layout.addWidget(self.progress)
1280
+
1281
+ self.result_lbl = QLabel("")
1282
+ self.result_lbl.setStyleSheet(f"color: {Brand.SUCCESS}; border: none; font-weight: bold; font-size: 14px;")
1283
+ layout.addWidget(self.result_lbl)
1284
+
1285
+ btn = QHBoxLayout()
1286
+ self.analyze_btn = QPushButton("Analyze")
1287
+ self.analyze_btn.clicked.connect(self._analyze)
1288
+ btn.addWidget(self.analyze_btn)
1289
+ self.clean_btn = QPushButton("Clean Now")
1290
+ self.clean_btn.setObjectName("primary")
1291
+ self.clean_btn.clicked.connect(self._clean)
1292
+ btn.addWidget(self.clean_btn)
1293
+ btn.addStretch()
1294
+ layout.addLayout(btn)
1295
+ layout.addStretch()
1296
+
1297
+ QTimer.singleShot(300, self._analyze)
1298
+ self._worker = None
1299
+
1300
+ def _analyze(self):
1301
+ size, count = DiskScanner.scan_temp_size()
1302
+ self.info_lbl.setText(f"Found {count:,} cleanable files ({format_size(size)})")
1303
+
1304
+ def _clean(self):
1305
+ if self.dry_run.isChecked():
1306
+ size, count = DiskScanner.scan_temp_size()
1307
+ QMessageBox.information(self, "Dry Run", f"Would clean ~{count:,} files\nWould free ~{format_size(size)}")
1308
+ return
1309
+
1310
+ if QMessageBox.question(self, "Clean", "Clean selected items?") != QMessageBox.StandardButton.Yes:
1311
+ return
1312
+
1313
+ tasks = []
1314
+ if self.chk_temp.isChecked():
1315
+ tasks.append('temp')
1316
+ if self.chk_trash.isChecked():
1317
+ tasks.append('trash')
1318
+ if self.chk_browser.isChecked():
1319
+ tasks.append('browser')
1320
+
1321
+ self.clean_btn.setEnabled(False)
1322
+ self.progress.setValue(50)
1323
+ self.progress.setFormat("Cleaning...")
1324
+
1325
+ self._worker = CleanWorker(tasks)
1326
+ self._worker.finished.connect(self._done)
1327
+ self._worker.start()
1328
+
1329
+ def _done(self, result):
1330
+ self.clean_btn.setEnabled(True)
1331
+ self.progress.setValue(100)
1332
+ self.progress.setFormat("Done!")
1333
+ self.result_lbl.setText(f"Cleaned {result['deleted']:,} files | Freed {format_size(result['freed'])}")
1334
+ self.log_signal.emit(f"Cleaned {result['deleted']} files, freed {format_size(result['freed'])}")
1335
+ ToastNotification(self.window(), f"Freed {format_size(result['freed'])}", "success")
1336
+ QTimer.singleShot(1000, self._analyze)
1337
+
1338
+
1339
+ class StartupTab(QWidget):
1340
+ log_signal = pyqtSignal(str)
1341
+
1342
+ def __init__(self):
1343
+ super().__init__()
1344
+ layout = QVBoxLayout(self)
1345
+ layout.setContentsMargins(12, 12, 12, 12)
1346
+ layout.setSpacing(10)
1347
+
1348
+ title = QLabel("Startup Manager")
1349
+ title.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold))
1350
+ title.setStyleSheet(f"color: {Brand.GOLD}; border: none;")
1351
+ layout.addWidget(title)
1352
+
1353
+ desc = QLabel("Programs that run when your computer starts")
1354
+ desc.setStyleSheet(f"color: {Brand.TEXT_DIM}; border: none;")
1355
+ layout.addWidget(desc)
1356
+
1357
+ self.table = QTableWidget()
1358
+ self.table.setColumnCount(4)
1359
+ self.table.setHorizontalHeaderLabels(["Name", "Command", "Source", "Status"])
1360
+ self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
1361
+ self.table.verticalHeader().setVisible(False)
1362
+ self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
1363
+ self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
1364
+ layout.addWidget(self.table)
1365
+
1366
+ bottom = QHBoxLayout()
1367
+ refresh_btn = QPushButton("Refresh")
1368
+ refresh_btn.clicked.connect(self._refresh)
1369
+ bottom.addWidget(refresh_btn)
1370
+ bottom.addStretch()
1371
+ self.status_lbl = QLabel("")
1372
+ self.status_lbl.setStyleSheet(f"color: {Brand.TEXT_DIM}; border: none;")
1373
+ bottom.addWidget(self.status_lbl)
1374
+ layout.addLayout(bottom)
1375
+
1376
+ self._refresh()
1377
+
1378
+ def _refresh(self):
1379
+ items = DiskScanner.get_startup_items()
1380
+ self.table.setRowCount(len(items))
1381
+ for i, item in enumerate(items):
1382
+ self.table.setItem(i, 0, QTableWidgetItem(item["name"]))
1383
+ self.table.setItem(i, 1, QTableWidgetItem(item["command"][:80]))
1384
+ self.table.setItem(i, 2, QTableWidgetItem(item["source"]))
1385
+ status = QTableWidgetItem("Enabled")
1386
+ status.setForeground(QColor(Brand.SUCCESS))
1387
+ self.table.setItem(i, 3, status)
1388
+ self.status_lbl.setText(f"{len(items)} startup items")
1389
+ self.log_signal.emit(f"Startup: {len(items)} items found")
1390
+
1391
+
1392
+ class LogTab(QWidget):
1393
+ def __init__(self):
1394
+ super().__init__()
1395
+ layout = QVBoxLayout(self)
1396
+ layout.setContentsMargins(12, 12, 12, 12)
1397
+
1398
+ title = QLabel("Activity Log")
1399
+ title.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold))
1400
+ title.setStyleSheet(f"color: {Brand.GOLD}; border: none;")
1401
+ layout.addWidget(title)
1402
+
1403
+ self.log = QTextEdit()
1404
+ self.log.setReadOnly(True)
1405
+ self.log.setFont(QFont("Consolas", 10))
1406
+ layout.addWidget(self.log)
1407
+
1408
+ btn = QHBoxLayout()
1409
+ QPushButton("Clear", clicked=self.log.clear).also = btn.addWidget(QPushButton("Clear", clicked=self.log.clear))
1410
+ btn.addStretch()
1411
+ layout.addLayout(btn)
1412
+
1413
+ self.add("MoneyPackCleaner initialized")
1414
+
1415
+ def add(self, msg):
1416
+ ts = datetime.now().strftime("%H:%M:%S")
1417
+ self.log.append(f"<span style='color:{Brand.TEXT_DIM}'>[{ts}]</span> <span style='color:{Brand.TEXT}'>{msg}</span>")
1418
+
1419
+
1420
+ # ============================================================================
1421
+ # MAIN WINDOW
1422
+ # ============================================================================
1423
+
1424
+ DARK_QSS = f"""
1425
+ QMainWindow {{ background-color: {Brand.BG_DARK}; color: {Brand.TEXT}; }}
1426
+ QMenuBar {{ background-color: {Brand.BG_PANEL}; color: {Brand.TEXT}; border-bottom: 1px solid {Brand.BORDER}; }}
1427
+ QMenuBar::item {{ padding: 6px 16px; }}
1428
+ QMenuBar::item:selected {{ background-color: {Brand.BG_CARD}; }}
1429
+ QMenu {{ background-color: {Brand.BG_PANEL}; border: 1px solid {Brand.BORDER}; color: {Brand.TEXT}; }}
1430
+ QMenu::item {{ padding: 6px 30px; }}
1431
+ QMenu::item:selected {{ background-color: {Brand.BG_CARD}; color: {Brand.GOLD}; }}
1432
+ QLabel {{ color: {Brand.TEXT}; }}
1433
+ QGroupBox {{ font-weight: bold; border: 1px solid {Brand.BORDER}; border-radius: 8px; margin-top: 10px; padding-top: 12px; color: {Brand.TEXT}; }}
1434
+ QGroupBox::title {{ subcontrol-origin: margin; left: 12px; padding: 0 5px; color: {Brand.GOLD}; }}
1435
+ QProgressBar {{ border: 1px solid {Brand.BORDER}; border-radius: 6px; text-align: center; background-color: {Brand.BG_PANEL}; color: {Brand.TEXT}; height: 22px; }}
1436
+ QProgressBar::chunk {{ background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 {Brand.PINK}, stop:1 {Brand.GOLD}); border-radius: 6px; }}
1437
+ QPushButton {{ background-color: {Brand.BG_CARD}; border: 1px solid {Brand.BORDER}; border-radius: 6px; padding: 8px 18px; color: {Brand.TEXT}; font-weight: bold; }}
1438
+ QPushButton:hover {{ border-color: {Brand.GOLD}; color: {Brand.GOLD}; }}
1439
+ QPushButton:pressed {{ background-color: {Brand.GOLD}; color: #000; }}
1440
+ QPushButton:disabled {{ color: {Brand.TEXT_MUTED}; border-color: {Brand.BG_PANEL}; }}
1441
+ QPushButton#primary {{ background-color: {Brand.PINK}; border: none; color: #fff; }}
1442
+ QPushButton#primary:hover {{ background-color: {Brand.PINK_LIGHT}; }}
1443
+ QPushButton#danger {{ background-color: {Brand.DANGER}; border: none; color: #fff; }}
1444
+ QPushButton#danger:hover {{ background-color: #ff4466; }}
1445
+ QTreeWidget, QTableWidget {{ background-color: {Brand.BG_PANEL}; border: 1px solid {Brand.BORDER}; color: {Brand.TEXT}; alternate-background-color: {Brand.BG_DARK}; gridline-color: {Brand.BORDER}; }}
1446
+ QTreeWidget::item:selected, QTableWidget::item:selected {{ background-color: {Brand.BG_CARD}; }}
1447
+ QHeaderView::section {{ background-color: {Brand.BG_CARD}; color: {Brand.TEXT_DIM}; padding: 6px; border: none; border-bottom: 2px solid {Brand.BORDER}; font-weight: bold; font-size: 11px; }}
1448
+ QTextEdit {{ background-color: {Brand.BG_PANEL}; border: 1px solid {Brand.BORDER}; color: {Brand.TEXT}; border-radius: 6px; }}
1449
+ QLineEdit {{ background-color: {Brand.BG_PANEL}; border: 1px solid {Brand.BORDER}; border-radius: 6px; padding: 7px; color: {Brand.TEXT}; }}
1450
+ QLineEdit:focus {{ border-color: {Brand.GOLD}; }}
1451
+ QTabWidget::pane {{ border: 1px solid {Brand.BORDER}; border-radius: 0; }}
1452
+ QTabBar::tab {{ background-color: {Brand.BG_PANEL}; color: {Brand.TEXT_DIM}; padding: 10px 20px; border: none; border-bottom: 3px solid transparent; font-weight: bold; }}
1453
+ QTabBar::tab:selected {{ color: {Brand.GOLD}; border-bottom: 3px solid {Brand.GOLD}; background-color: {Brand.BG_DARK}; }}
1454
+ QTabBar::tab:hover {{ color: {Brand.TEXT}; }}
1455
+ QScrollBar:vertical {{ background-color: {Brand.BG_DARK}; width: 8px; }}
1456
+ QScrollBar::handle:vertical {{ background-color: {Brand.BORDER}; border-radius: 4px; min-height: 20px; }}
1457
+ QScrollBar::handle:vertical:hover {{ background-color: {Brand.GOLD}; }}
1458
+ QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }}
1459
+ QCheckBox {{ color: {Brand.TEXT}; spacing: 8px; }}
1460
+ QCheckBox::indicator {{ width: 18px; height: 18px; border: 2px solid {Brand.BORDER}; border-radius: 4px; background: {Brand.BG_PANEL}; }}
1461
+ QCheckBox::indicator:checked {{ background-color: {Brand.PINK}; border-color: {Brand.PINK}; }}
1462
+ QSpinBox {{ background: {Brand.BG_PANEL}; border: 1px solid {Brand.BORDER}; border-radius: 6px; padding: 5px; color: {Brand.TEXT}; }}
1463
+ QStatusBar {{ background-color: {Brand.BG_PANEL}; color: {Brand.TEXT_DIM}; border-top: 1px solid {Brand.BORDER}; }}
1464
+ QFrame {{ border: none; }}
1465
+ QToolTip {{ background: {Brand.BG_CARD}; color: {Brand.TEXT}; border: 1px solid {Brand.GOLD}; padding: 5px; border-radius: 4px; }}
1466
+ QMessageBox {{ background-color: {Brand.BG_DARK}; }}
1467
+ """
1468
+
1469
+
1470
+ class MoneyPackCleanerWindow(QMainWindow):
1471
+ def __init__(self):
1472
+ super().__init__()
1473
+ self.setWindowTitle("MoneyPackCleaner")
1474
+ self.setMinimumSize(1100, 750)
1475
+ self.resize(1300, 850)
1476
+
1477
+ self._setup_menu()
1478
+ self._setup_ui()
1479
+ self._setup_tray()
1480
+ self._setup_statusbar()
1481
+ self._setup_scheduler()
1482
+
1483
+ def _setup_menu(self):
1484
+ mb = self.menuBar()
1485
+ file_m = mb.addMenu("File")
1486
+ file_m.addAction(QAction("Refresh", self, shortcut="F5", triggered=lambda: self.dashboard.refresh()))
1487
+ file_m.addSeparator()
1488
+ file_m.addAction(QAction("Exit", self, shortcut="Ctrl+Q", triggered=self.close))
1489
+
1490
+ tools_m = mb.addMenu("Tools")
1491
+ tools_m.addAction(QAction("Quick Clean", self, shortcut="Ctrl+Shift+C", triggered=lambda: self.dashboard._quick_clean()))
1492
+ tools_m.addSeparator()
1493
+ for i, name in enumerate(["Dashboard","Large Files","Old Files","Duplicates","Clean","Startup","Log"]):
1494
+ tools_m.addAction(QAction(name, self, triggered=lambda checked, idx=i: self.tabs.setCurrentIndex(idx)))
1495
+
1496
+ help_m = mb.addMenu("Help")
1497
+ help_m.addAction(QAction("About", self, triggered=self._about))
1498
+
1499
+ def _setup_ui(self):
1500
+ central = QWidget()
1501
+ self.setCentralWidget(central)
1502
+ layout = QVBoxLayout(central)
1503
+ layout.setContentsMargins(0, 0, 0, 0)
1504
+
1505
+ self.tabs = QTabWidget()
1506
+
1507
+ self.dashboard = DashboardTab()
1508
+ self.tabs.addTab(self.dashboard, "Dashboard")
1509
+
1510
+ self.large_tab = LargeFilesTab()
1511
+ self.tabs.addTab(self.large_tab, "Large Files")
1512
+
1513
+ self.old_tab = OldFilesTab()
1514
+ self.tabs.addTab(self.old_tab, "Old Files")
1515
+
1516
+ self.dup_tab = DuplicatesTab()
1517
+ self.tabs.addTab(self.dup_tab, "Duplicates")
1518
+
1519
+ self.clean_tab = CleanerTab()
1520
+ self.tabs.addTab(self.clean_tab, "Clean")
1521
+
1522
+ self.startup_tab = StartupTab()
1523
+ self.tabs.addTab(self.startup_tab, "Startup")
1524
+
1525
+ self.log_tab = LogTab()
1526
+ self.tabs.addTab(self.log_tab, "Log")
1527
+
1528
+ layout.addWidget(self.tabs)
1529
+
1530
+ # Connect logs
1531
+ for tab in [self.dashboard, self.large_tab, self.old_tab, self.dup_tab, self.clean_tab, self.startup_tab]:
1532
+ tab.log_signal.connect(self.log_tab.add)
1533
+
1534
+ def _setup_tray(self):
1535
+ if not QSystemTrayIcon.isSystemTrayAvailable():
1536
+ return
1537
+
1538
+ # Create icon
1539
+ pixmap = QPixmap(32, 32)
1540
+ pixmap.fill(QColor(0, 0, 0, 0))
1541
+ painter = QPainter(pixmap)
1542
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
1543
+ painter.setBrush(QColor(Brand.PINK))
1544
+ painter.setPen(Qt.PenStyle.NoPen)
1545
+ painter.drawEllipse(2, 2, 28, 28)
1546
+ painter.setPen(QPen(QColor(Brand.GOLD), 2))
1547
+ painter.setFont(QFont("Arial", 14, QFont.Weight.Bold))
1548
+ painter.drawText(QRect(0, 0, 32, 32), Qt.AlignmentFlag.AlignCenter, "M")
1549
+ painter.end()
1550
+
1551
+ self._tray = QSystemTrayIcon(QIcon(pixmap), self)
1552
+ self._tray.setToolTip("MoneyPackCleaner")
1553
+
1554
+ menu = QMenu()
1555
+ menu.addAction("Show", self.show)
1556
+ menu.addAction("Quick Clean", self.dashboard._quick_clean)
1557
+ menu.addSeparator()
1558
+ menu.addAction("Exit", self._quit)
1559
+ self._tray.setContextMenu(menu)
1560
+ self._tray.activated.connect(lambda r: self.show() if r == QSystemTrayIcon.ActivationReason.DoubleClick else None)
1561
+ self._tray.show()
1562
+
1563
+ def _setup_statusbar(self):
1564
+ self.statusBar().showMessage("Ready")
1565
+ self._status_timer = QTimer(self)
1566
+ self._status_timer.timeout.connect(self._update_status)
1567
+ self._status_timer.start(15000)
1568
+ self._update_status()
1569
+
1570
+ def _update_status(self):
1571
+ disks = DiskScanner.get_disks()
1572
+ if disks:
1573
+ free = sum(d.free for d in disks)
1574
+ self.statusBar().showMessage(f"Free: {format_size(free)} | {datetime.now().strftime('%H:%M')}")
1575
+
1576
+ def _setup_scheduler(self):
1577
+ """Auto-clean every 4 hours."""
1578
+ self._sched_timer = QTimer(self)
1579
+ self._sched_timer.timeout.connect(self._scheduled_clean)
1580
+ self._sched_timer.start(4 * 60 * 60 * 1000) # 4 hours
1581
+
1582
+ def _scheduled_clean(self):
1583
+ r = DiskCleaner.clean_temp()
1584
+ self.log_tab.add(f"[Scheduled] Auto-cleaned {r['deleted']} files, freed {format_size(r['freed'])}")
1585
+ if hasattr(self, '_tray'):
1586
+ self._tray.showMessage("MoneyPackCleaner", f"Auto-cleaned: freed {format_size(r['freed'])}", QSystemTrayIcon.MessageIcon.Information, 3000)
1587
+
1588
+ def _about(self):
1589
+ QMessageBox.about(self, "About MoneyPackCleaner",
1590
+ "MoneyPackCleaner v1.0\n"
1591
+ "Created by MoneyPack\n\n"
1592
+ "Premium Storage Optimization Suite\n\n"
1593
+ "Features:\n"
1594
+ "- Disk usage with donut chart\n"
1595
+ "- Large file scanner\n"
1596
+ "- Forgotten file finder\n"
1597
+ "- Duplicate detector\n"
1598
+ "- System cleaner\n"
1599
+ "- Startup manager\n"
1600
+ "- Scheduled auto-cleaning\n"
1601
+ "- System tray\n"
1602
+ "- Dry run mode\n"
1603
+ "- Toast notifications"
1604
+ )
1605
+
1606
+ def _quit(self):
1607
+ if hasattr(self, '_tray'):
1608
+ self._tray.hide()
1609
+ QApplication.quit()
1610
+
1611
+ def closeEvent(self, event):
1612
+ if hasattr(self, '_tray') and self._tray.isVisible():
1613
+ self.hide()
1614
+ self._tray.showMessage("MoneyPackCleaner", "Running in background", QSystemTrayIcon.MessageIcon.Information, 2000)
1615
+ event.ignore()
1616
+ else:
1617
+ event.accept()
1618
+
1619
+
1620
+ # ============================================================================
1621
+ # SPLASH SCREEN
1622
+ # ============================================================================
1623
+
1624
+ def create_splash() -> QSplashScreen:
1625
+ """Create branded splash screen."""
1626
+ pixmap = QPixmap(500, 300)
1627
+ pixmap.fill(QColor(Brand.BG_DARK))
1628
+
1629
+ painter = QPainter(pixmap)
1630
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
1631
+
1632
+ # Gradient background
1633
+ grad = QLinearGradient(0, 0, 500, 300)
1634
+ grad.setColorAt(0, QColor(Brand.BG_DARK))
1635
+ grad.setColorAt(0.5, QColor(Brand.BG_PANEL))
1636
+ grad.setColorAt(1, QColor(Brand.BG_DARK))
1637
+ painter.fillRect(0, 0, 500, 300, grad)
1638
+
1639
+ # Border glow
1640
+ painter.setPen(QPen(QColor(Brand.GOLD), 2))
1641
+ painter.setBrush(Qt.BrushStyle.NoBrush)
1642
+ painter.drawRoundedRect(2, 2, 496, 296, 12, 12)
1643
+
1644
+ # Bunny silhouette (centered)
1645
+ painter.setBrush(QColor(Brand.PINK))
1646
+ painter.setPen(Qt.PenStyle.NoPen)
1647
+ cx, cy = 250, 110
1648
+ # Head
1649
+ painter.drawEllipse(int(cx-25), int(cy-25), 50, 50)
1650
+ # Ears
1651
+ painter.drawEllipse(int(cx-18), int(cy-70), 14, 50)
1652
+ painter.drawEllipse(int(cx+4), int(cy-70), 14, 50)
1653
+ # Inner ears (gold)
1654
+ painter.setBrush(QColor(Brand.GOLD))
1655
+ painter.drawEllipse(int(cx-14), int(cy-60), 7, 30)
1656
+ painter.drawEllipse(int(cx+7), int(cy-60), 7, 30)
1657
+
1658
+ # Title
1659
+ painter.setPen(QColor(Brand.GOLD))
1660
+ font = QFont("Segoe UI", 28, QFont.Weight.Bold)
1661
+ painter.setFont(font)
1662
+ painter.drawText(QRect(0, 150, 500, 50), Qt.AlignmentFlag.AlignCenter, "MoneyPackCleaner")
1663
+
1664
+ # Subtitle
1665
+ painter.setPen(QColor(Brand.TEXT_DIM))
1666
+ font = QFont("Segoe UI", 12)
1667
+ painter.setFont(font)
1668
+ painter.drawText(QRect(0, 200, 500, 30), Qt.AlignmentFlag.AlignCenter, "Premium Storage Optimization")
1669
+
1670
+ # Author
1671
+ painter.setPen(QColor(Brand.PINK))
1672
+ font = QFont("Segoe UI", 10)
1673
+ painter.setFont(font)
1674
+ painter.drawText(QRect(0, 240, 500, 25), Qt.AlignmentFlag.AlignCenter, "by MoneyPack")
1675
+
1676
+ # Loading text
1677
+ painter.setPen(QColor(Brand.TEXT_MUTED))
1678
+ painter.drawText(QRect(0, 270, 500, 20), Qt.AlignmentFlag.AlignCenter, "Loading...")
1679
+
1680
+ painter.end()
1681
+
1682
+ splash = QSplashScreen(pixmap)
1683
+ splash.setWindowFlags(Qt.WindowType.SplashScreen | Qt.WindowType.FramelessWindowHint)
1684
+ return splash
1685
+
1686
+
1687
+ # ============================================================================
1688
+ # ENTRY POINT
1689
+ # ============================================================================
1690
+
1691
+ def main():
1692
+ app = QApplication(sys.argv)
1693
+ app.setStyle("Fusion")
1694
+ app.setStyleSheet(DARK_QSS)
1695
+ app.setFont(QFont("Segoe UI", 10))
1696
+ app.setApplicationName("MoneyPackCleaner")
1697
+
1698
+ # Splash
1699
+ splash = create_splash()
1700
+ splash.show()
1701
+ app.processEvents()
1702
+
1703
+ # Load main window
1704
+ time.sleep(1.5)
1705
+ window = MoneyPackCleanerWindow()
1706
+
1707
+ splash.finish(window)
1708
+ window.show()
1709
+
1710
+ sys.exit(app.exec())
1711
+
1712
+
1713
+ if __name__ == "__main__":
1714
+ main()