MoneyPack commited on
Commit
e39c166
·
verified ·
1 Parent(s): 27e4920

MoneyPackCleaner ULTRA - Premium frameless UI with particles, glass cards, ring progress, animated sidebar

Browse files
Files changed (1) hide show
  1. moneypack_cleaner_ultra.py +1256 -0
moneypack_cleaner_ultra.py ADDED
@@ -0,0 +1,1256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ MoneyPackCleaner Ultra v2.0
5
+ Premium Storage Optimization Suite - Elite Edition
6
+ Created by MoneyPack
7
+
8
+ DESIGN: Frameless window, glassmorphism, animated gradients,
9
+ particle effects, neon glow, custom everything.
10
+ """
11
+
12
+ APP_VERSION = "2.0.0"
13
+ UPDATE_URL = "https://huggingface.co/MoneyPack/MoneyPack-Security-Suite/raw/main/moneypack_cleaner.py"
14
+ VERSION_CHECK_URL = "https://huggingface.co/MoneyPack/MoneyPack-Security-Suite/raw/main/version.json"
15
+
16
+ import os
17
+ import sys
18
+ import time
19
+ import math
20
+ import shutil
21
+ import hashlib
22
+ import platform
23
+ import subprocess
24
+ import json
25
+ import random
26
+ import urllib.request
27
+ from pathlib import Path
28
+ from datetime import datetime, timedelta
29
+ from dataclasses import dataclass, field
30
+ from typing import List, Tuple, Dict
31
+ from collections import defaultdict
32
+
33
+ from PyQt6.QtWidgets import (
34
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
35
+ QLabel, QPushButton, QProgressBar, QCheckBox, QStackedWidget,
36
+ QTreeWidget, QTreeWidgetItem, QTableWidget, QTableWidgetItem,
37
+ QHeaderView, QAbstractItemView, QTextEdit, QLineEdit,
38
+ QFileDialog, QMessageBox, QGroupBox, QFrame, QSpinBox,
39
+ QSystemTrayIcon, QMenu, QSizePolicy, QGraphicsDropShadowEffect,
40
+ QScrollArea
41
+ )
42
+ from PyQt6.QtCore import (
43
+ Qt, QThread, pyqtSignal, QTimer, QSize, QPropertyAnimation,
44
+ QPoint, QRect, QEasingCurve, QPointF, pyqtProperty, QRectF
45
+ )
46
+ from PyQt6.QtGui import (
47
+ QFont, QColor, QAction, QIcon, QPainter, QPen, QBrush,
48
+ QLinearGradient, QRadialGradient, QPixmap, QPainterPath,
49
+ QConicalGradient, QMouseEvent, QRegion, QPaintEvent
50
+ )
51
+
52
+
53
+ # ============================================================================
54
+ # BRAND PALETTE - Luxury Dark
55
+ # ============================================================================
56
+
57
+ class C:
58
+ """Color constants - elite dark luxury palette."""
59
+ # Backgrounds
60
+ BG = "#08080f"
61
+ BG2 = "#0e0e1a"
62
+ PANEL = "#13132a"
63
+ CARD = "#1a1a35"
64
+ CARD_HOVER = "#222245"
65
+
66
+ # Accents
67
+ GOLD = "#d4af37"
68
+ GOLD_BRIGHT = "#ffd700"
69
+ PINK = "#ff2d75"
70
+ PINK_SOFT = "#ff5c94"
71
+ CYAN = "#00e5ff"
72
+ PURPLE = "#9c4dff"
73
+ BLUE = "#4d7cff"
74
+
75
+ # Gradients
76
+ GRAD_START = "#ff2d75"
77
+ GRAD_END = "#d4a f37"
78
+
79
+ # Text
80
+ WHITE = "#f0f0ff"
81
+ TEXT = "#c8c8e0"
82
+ DIM = "#6a6a8a"
83
+ MUTED = "#3a3a55"
84
+
85
+ # Status
86
+ GREEN = "#00e676"
87
+ YELLOW = "#ffab00"
88
+ RED = "#ff1744"
89
+
90
+ # Glass
91
+ GLASS = "#ffffff08"
92
+ GLASS_BORDER = "#ffffff15"
93
+
94
+
95
+ # ============================================================================
96
+ # UTILITY
97
+ # ============================================================================
98
+
99
+ def format_size(b: int) -> str:
100
+ if b <= 0:
101
+ return "0 B"
102
+ for u in ['B','KB','MB','GB','TB']:
103
+ if b < 1024:
104
+ return f"{b:.1f} {u}"
105
+ b /= 1024
106
+ return f"{b:.1f} PB"
107
+
108
+
109
+ # ============================================================================
110
+ # CORE ENGINE (same logic, condensed)
111
+ # ============================================================================
112
+
113
+ class Engine:
114
+ SKIP = {'$Recycle.Bin','System Volume Information','Windows','WinSxS','node_modules','.git','__pycache__','venv','.venv','Recovery','proc','sys','dev'}
115
+ CATS = {'Video':{'.mp4','.avi','.mkv','.mov','.wmv','.webm'},'Audio':{'.mp3','.wav','.flac','.aac','.ogg','.m4a'},'Image':{'.jpg','.jpeg','.png','.gif','.bmp','.webp','.psd'},'Archive':{'.zip','.rar','.7z','.tar','.gz','.iso'},'Document':{'.pdf','.doc','.docx','.xls','.xlsx','.ppt','.pptx','.txt'},'Installer':{'.exe','.msi','.dmg','.deb','.apk'}}
116
+
117
+ @classmethod
118
+ def category(cls, ext):
119
+ ext = ext.lower()
120
+ for cat, exts in cls.CATS.items():
121
+ if ext in exts:
122
+ return cat
123
+ return "Other"
124
+
125
+ @classmethod
126
+ def get_disks(cls):
127
+ disks = []
128
+ if sys.platform == 'win32':
129
+ try:
130
+ import ctypes
131
+ bm = ctypes.windll.kernel32.GetLogicalDrives()
132
+ for l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
133
+ if bm & 1:
134
+ d = f"{l}:\\"
135
+ try:
136
+ u = shutil.disk_usage(d)
137
+ if u.total > 0:
138
+ disks.append({'drive':f"{l}:",'total':u.total,'used':u.used,'free':u.free,'pct':round(u.used/u.total*100,1)})
139
+ except:
140
+ pass
141
+ bm >>= 1
142
+ except:
143
+ pass
144
+ else:
145
+ seen = set()
146
+ for m in ['/', str(Path.home())]:
147
+ try:
148
+ u = shutil.disk_usage(m)
149
+ if u.total not in seen and u.total > 0:
150
+ seen.add(u.total)
151
+ disks.append({'drive':m,'total':u.total,'used':u.used,'free':u.free,'pct':round(u.used/u.total*100,1)})
152
+ except:
153
+ pass
154
+ return disks
155
+
156
+ @classmethod
157
+ def scan_large(cls, root, threshold=50*1024*1024, max_n=300, cb=None):
158
+ results = []
159
+ n = 0
160
+ for dp, dns, fns in os.walk(root):
161
+ dns[:] = [d for d in dns if d not in cls.SKIP and not d.startswith('.')]
162
+ for f in fns:
163
+ try:
164
+ fp = os.path.join(dp, f)
165
+ sz = os.path.getsize(fp)
166
+ if sz >= threshold:
167
+ ext = os.path.splitext(f)[1].lower()
168
+ results.append({'path':fp,'size':sz,'ext':ext,'cat':cls.category(ext),'name':f})
169
+ n += 1
170
+ if cb and n % 20 == 0: cb(n)
171
+ if n >= max_n: return sorted(results, key=lambda x:x['size'], reverse=True)
172
+ except:
173
+ continue
174
+ return sorted(results, key=lambda x:x['size'], reverse=True)
175
+
176
+ @classmethod
177
+ def scan_old(cls, root, min_days=180, min_size=1024*1024, max_n=200, cb=None):
178
+ results = []
179
+ n = 0
180
+ now = time.time()
181
+ for dp, dns, fns in os.walk(root):
182
+ dns[:] = [d for d in dns if d not in cls.SKIP and not d.startswith('.')]
183
+ for f in fns:
184
+ try:
185
+ fp = os.path.join(dp, f)
186
+ sz = os.path.getsize(fp)
187
+ if sz >= min_size:
188
+ days = int((now - os.path.getatime(fp)) / 86400)
189
+ if days >= min_days:
190
+ results.append({'path':fp,'size':sz,'days':days,'name':f,'cat':cls.category(os.path.splitext(f)[1])})
191
+ n += 1
192
+ if cb and n % 20 == 0: cb(n)
193
+ if n >= max_n: return sorted(results, key=lambda x:x['days'], reverse=True)
194
+ except:
195
+ continue
196
+ return sorted(results, key=lambda x:x['days'], reverse=True)
197
+
198
+ @classmethod
199
+ def find_dupes(cls, root, min_size=1024*1024, cb=None):
200
+ sm = defaultdict(list)
201
+ for dp, dns, fns in os.walk(root):
202
+ dns[:] = [d for d in dns if d not in cls.SKIP and not d.startswith('.')]
203
+ for f in fns:
204
+ try:
205
+ fp = os.path.join(dp, f)
206
+ sz = os.path.getsize(fp)
207
+ if sz >= min_size: sm[sz].append(fp)
208
+ except:
209
+ pass
210
+
211
+ groups = []
212
+ hm = defaultdict(list)
213
+ for sz, paths in sm.items():
214
+ if len(paths) < 2: continue
215
+ for fp in paths:
216
+ try:
217
+ h = hashlib.md5()
218
+ with open(fp,'rb') as f:
219
+ h.update(f.read(8192))
220
+ if sz > 8192:
221
+ f.seek(-8192,2)
222
+ h.update(f.read(8192))
223
+ hm[f"{sz}_{h.hexdigest()}"].append(fp)
224
+ except:
225
+ pass
226
+
227
+ for k, ps in hm.items():
228
+ if len(ps) > 1:
229
+ groups.append({'size':int(k.split('_')[0]),'files':ps})
230
+ return sorted(groups, key=lambda x:x['size']*len(x['files']), reverse=True)
231
+
232
+ @classmethod
233
+ def temp_size(cls):
234
+ paths = []
235
+ if sys.platform == 'win32':
236
+ paths = [os.environ.get('TEMP',''), os.path.join(os.environ.get('LOCALAPPDATA',''),'Temp')]
237
+ else:
238
+ paths = ['/tmp', os.path.join(str(Path.home()),'.cache')]
239
+ total, count = 0, 0
240
+ for d in paths:
241
+ if not d or not os.path.exists(d): continue
242
+ try:
243
+ for r,_,fs in os.walk(d):
244
+ for f in fs:
245
+ try:
246
+ total += os.path.getsize(os.path.join(r,f))
247
+ count += 1
248
+ except: pass
249
+ except: pass
250
+ return total, count
251
+
252
+ @classmethod
253
+ def clean_temp(cls, cb=None):
254
+ result = {'deleted':0,'freed':0}
255
+ paths = []
256
+ if sys.platform == 'win32':
257
+ paths = [os.environ.get('TEMP',''), os.path.join(os.environ.get('LOCALAPPDATA',''),'Temp'), os.path.join(os.environ.get('WINDIR',r'C:\Windows'),'Temp')]
258
+ else:
259
+ paths = ['/tmp', os.path.join(str(Path.home()),'.cache')]
260
+ for d in paths:
261
+ if not d or not os.path.exists(d): continue
262
+ try:
263
+ for r,_,fs in os.walk(d):
264
+ for f in fs:
265
+ fp = os.path.join(r,f)
266
+ try:
267
+ sz = os.path.getsize(fp)
268
+ os.remove(fp)
269
+ result['deleted'] += 1
270
+ result['freed'] += sz
271
+ if cb and result['deleted'] % 50 == 0: cb(result['deleted'])
272
+ except: pass
273
+ except: pass
274
+ return result
275
+
276
+ @classmethod
277
+ def delete_files(cls, paths):
278
+ r = {'deleted':0,'freed':0,'failed':0}
279
+ for p in paths:
280
+ try:
281
+ if os.path.isfile(p):
282
+ sz = os.path.getsize(p)
283
+ os.remove(p)
284
+ r['deleted'] += 1; r['freed'] += sz
285
+ elif os.path.isdir(p):
286
+ shutil.rmtree(p)
287
+ r['deleted'] += 1
288
+ else: r['failed'] += 1
289
+ except: r['failed'] += 1
290
+ return r
291
+
292
+
293
+ # ============================================================================
294
+ # WORKER
295
+ # ============================================================================
296
+
297
+ class Worker(QThread):
298
+ progress = pyqtSignal(object)
299
+ done = pyqtSignal(object)
300
+
301
+ def __init__(self, fn, *args, **kwargs):
302
+ super().__init__()
303
+ self.fn = fn
304
+ self.args = args
305
+ self.kwargs = kwargs
306
+
307
+ def run(self):
308
+ result = self.fn(*self.args, **self.kwargs)
309
+ self.done.emit(result)
310
+
311
+
312
+ # ============================================================================
313
+ # CUSTOM WIDGETS - PARTICLE BACKGROUND
314
+ # ============================================================================
315
+
316
+ class ParticleBackground(QWidget):
317
+ """Subtle animated floating particles."""
318
+
319
+ def __init__(self, parent=None):
320
+ super().__init__(parent)
321
+ self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
322
+ self._particles = []
323
+ for _ in range(30):
324
+ self._particles.append({
325
+ 'x': random.random(), 'y': random.random(),
326
+ 'vx': (random.random() - 0.5) * 0.001,
327
+ 'vy': (random.random() - 0.5) * 0.001,
328
+ 'size': random.uniform(1, 3),
329
+ 'alpha': random.randint(20, 60),
330
+ })
331
+ self._timer = QTimer(self)
332
+ self._timer.timeout.connect(self._tick)
333
+ self._timer.start(50)
334
+
335
+ def _tick(self):
336
+ for p in self._particles:
337
+ p['x'] += p['vx']
338
+ p['y'] += p['vy']
339
+ if p['x'] < 0 or p['x'] > 1: p['vx'] *= -1
340
+ if p['y'] < 0 or p['y'] > 1: p['vy'] *= -1
341
+ self.update()
342
+
343
+ def paintEvent(self, event):
344
+ painter = QPainter(self)
345
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
346
+ w, h = self.width(), self.height()
347
+ for p in self._particles:
348
+ color = QColor(C.GOLD)
349
+ color.setAlpha(p['alpha'])
350
+ painter.setBrush(color)
351
+ painter.setPen(Qt.PenStyle.NoPen)
352
+ painter.drawEllipse(QPointF(p['x']*w, p['y']*h), p['size'], p['size'])
353
+ painter.end()
354
+
355
+
356
+ # ============================================================================
357
+ # CUSTOM WIDGETS - FRAMELESS TITLEBAR
358
+ # ============================================================================
359
+
360
+ class TitleBar(QWidget):
361
+ """Custom frameless window title bar."""
362
+
363
+ def __init__(self, parent_window):
364
+ super().__init__()
365
+ self.parent_window = parent_window
366
+ self._drag_pos = None
367
+ self.setFixedHeight(42)
368
+ self.setStyleSheet(f"background-color: {C.BG}; border-bottom: 1px solid {C.MUTED};")
369
+
370
+ layout = QHBoxLayout(self)
371
+ layout.setContentsMargins(12, 0, 8, 0)
372
+ layout.setSpacing(8)
373
+
374
+ # Logo dot
375
+ dot = QLabel()
376
+ dot.setFixedSize(14, 14)
377
+ dot.setStyleSheet(f"background-color: {C.PINK}; border-radius: 7px; border: none;")
378
+ layout.addWidget(dot)
379
+
380
+ # Title
381
+ title = QLabel("MoneyPackCleaner")
382
+ title.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold))
383
+ title.setStyleSheet(f"color: {C.GOLD}; border: none;")
384
+ layout.addWidget(title)
385
+
386
+ # Version
387
+ ver = QLabel(f"v{APP_VERSION}")
388
+ ver.setStyleSheet(f"color: {C.MUTED}; font-size: 9px; border: none;")
389
+ layout.addWidget(ver)
390
+
391
+ layout.addStretch()
392
+
393
+ # Window controls
394
+ for text, color, action in [("—", C.DIM, "min"), ("□", C.DIM, "max"), ("×", C.RED, "close")]:
395
+ btn = QPushButton(text)
396
+ btn.setFixedSize(32, 28)
397
+ btn.setStyleSheet(f"""
398
+ QPushButton {{ background: transparent; color: {color}; border: none; font-size: 14px; border-radius: 4px; }}
399
+ QPushButton:hover {{ background-color: {C.CARD}; color: {C.WHITE}; }}
400
+ """)
401
+ if action == "min":
402
+ btn.clicked.connect(parent_window.showMinimized)
403
+ elif action == "max":
404
+ btn.clicked.connect(self._toggle_max)
405
+ elif action == "close":
406
+ btn.clicked.connect(parent_window.close)
407
+ btn.setStyleSheet(f"""
408
+ QPushButton {{ background: transparent; color: {color}; border: none; font-size: 16px; border-radius: 4px; }}
409
+ QPushButton:hover {{ background-color: {C.RED}; color: white; }}
410
+ """)
411
+ layout.addWidget(btn)
412
+
413
+ def _toggle_max(self):
414
+ if self.parent_window.isMaximized():
415
+ self.parent_window.showNormal()
416
+ else:
417
+ self.parent_window.showMaximized()
418
+
419
+ def mousePressEvent(self, event: QMouseEvent):
420
+ if event.button() == Qt.MouseButton.LeftButton:
421
+ self._drag_pos = event.globalPosition().toPoint() - self.parent_window.frameGeometry().topLeft()
422
+
423
+ def mouseMoveEvent(self, event: QMouseEvent):
424
+ if self._drag_pos and event.buttons() == Qt.MouseButton.LeftButton:
425
+ self.parent_window.move(event.globalPosition().toPoint() - self._drag_pos)
426
+
427
+ def mouseReleaseEvent(self, event):
428
+ self._drag_pos = None
429
+
430
+ def mouseDoubleClickEvent(self, event):
431
+ self._toggle_max()
432
+
433
+
434
+ # ============================================================================
435
+ # CUSTOM WIDGETS - SIDEBAR
436
+ # ============================================================================
437
+
438
+ class SidebarButton(QPushButton):
439
+ """Sidebar navigation button with glow effect."""
440
+
441
+ def __init__(self, icon_text, label, index):
442
+ super().__init__()
443
+ self.index = index
444
+ self._icon = icon_text
445
+ self._label = label
446
+ self._active = False
447
+ self._hover = False
448
+ self.setFixedHeight(48)
449
+ self.setFixedWidth(200)
450
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
451
+ self.setStyleSheet("border: none; background: transparent;")
452
+
453
+ def set_active(self, active):
454
+ self._active = active
455
+ self.update()
456
+
457
+ def enterEvent(self, event):
458
+ self._hover = True
459
+ self.update()
460
+
461
+ def leaveEvent(self, event):
462
+ self._hover = False
463
+ self.update()
464
+
465
+ def paintEvent(self, event):
466
+ p = QPainter(self)
467
+ p.setRenderHint(QPainter.RenderHint.Antialiasing)
468
+
469
+ w, h = self.width(), self.height()
470
+
471
+ # Background
472
+ if self._active:
473
+ grad = QLinearGradient(0, 0, w, 0)
474
+ grad.setColorAt(0, QColor(C.PINK + "30"))
475
+ grad.setColorAt(1, QColor(C.PINK + "05"))
476
+ p.fillRect(0, 0, w, h, grad)
477
+ # Left accent bar
478
+ p.fillRect(0, 4, 3, h - 8, QColor(C.PINK))
479
+ elif self._hover:
480
+ p.fillRect(0, 0, w, h, QColor(C.CARD + "80"))
481
+
482
+ # Icon
483
+ p.setPen(QColor(C.PINK if self._active else C.DIM))
484
+ p.setFont(QFont("Segoe UI Emoji", 16))
485
+ p.drawText(QRect(16, 0, 30, h), Qt.AlignmentFlag.AlignVCenter, self._icon)
486
+
487
+ # Label
488
+ p.setPen(QColor(C.WHITE if self._active else C.TEXT))
489
+ p.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold if self._active else QFont.Weight.Normal))
490
+ p.drawText(QRect(52, 0, w - 60, h), Qt.AlignmentFlag.AlignVCenter, self._label)
491
+
492
+ p.end()
493
+
494
+
495
+ class Sidebar(QWidget):
496
+ """Animated sidebar navigation."""
497
+ page_changed = pyqtSignal(int)
498
+
499
+ def __init__(self):
500
+ super().__init__()
501
+ self.setFixedWidth(200)
502
+ self.setStyleSheet(f"background-color: {C.BG2}; border-right: 1px solid {C.MUTED};")
503
+
504
+ layout = QVBoxLayout(self)
505
+ layout.setContentsMargins(0, 16, 0, 16)
506
+ layout.setSpacing(2)
507
+
508
+ # Brand header
509
+ brand = QWidget()
510
+ brand.setFixedHeight(60)
511
+ brand.setStyleSheet("border: none;")
512
+ bl = QHBoxLayout(brand)
513
+ bl.setContentsMargins(16, 0, 0, 0)
514
+
515
+ # Mini bunny
516
+ bunny_dot = QLabel()
517
+ bunny_dot.setFixedSize(32, 32)
518
+ bunny_dot.setStyleSheet(f"background: qradialgradient(cx:0.5,cy:0.5,radius:0.5, fx:0.5,fy:0.5, stop:0 {C.PINK}, stop:1 {C.PURPLE}); border-radius: 16px; border: 2px solid {C.GOLD};")
519
+ bl.addWidget(bunny_dot)
520
+
521
+ brand_text = QVBoxLayout()
522
+ brand_text.setSpacing(0)
523
+ bn = QLabel("MoneyPack")
524
+ bn.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold))
525
+ bn.setStyleSheet(f"color: {C.GOLD}; border: none;")
526
+ brand_text.addWidget(bn)
527
+ bs = QLabel("CLEANER")
528
+ bs.setStyleSheet(f"color: {C.DIM}; font-size: 9px; letter-spacing: 3px; border: none;")
529
+ brand_text.addWidget(bs)
530
+ bl.addLayout(brand_text)
531
+ bl.addStretch()
532
+ layout.addWidget(brand)
533
+
534
+ layout.addSpacing(20)
535
+
536
+ # Nav buttons
537
+ self._buttons = []
538
+ nav_items = [
539
+ ("🏠", "Dashboard"),
540
+ ("📁", "Large Files"),
541
+ ("⏰", "Old Files"),
542
+ ("📋", "Duplicates"),
543
+ ("🧹", "Clean"),
544
+ ("📊", "Log"),
545
+ ]
546
+
547
+ for i, (icon, label) in enumerate(nav_items):
548
+ btn = SidebarButton(icon, label, i)
549
+ btn.clicked.connect(lambda checked, idx=i: self._on_click(idx))
550
+ layout.addWidget(btn)
551
+ self._buttons.append(btn)
552
+
553
+ layout.addStretch()
554
+
555
+ # Bottom version
556
+ ver = QLabel(f"v{APP_VERSION}")
557
+ ver.setAlignment(Qt.AlignmentFlag.AlignCenter)
558
+ ver.setStyleSheet(f"color: {C.MUTED}; font-size: 10px; border: none;")
559
+ layout.addWidget(ver)
560
+
561
+ self._buttons[0].set_active(True)
562
+
563
+ def _on_click(self, index):
564
+ for btn in self._buttons:
565
+ btn.set_active(btn.index == index)
566
+ self.page_changed.emit(index)
567
+
568
+
569
+ # ============================================================================
570
+ # CUSTOM WIDGETS - GLASS CARD
571
+ # ============================================================================
572
+
573
+ class GlassCard(QFrame):
574
+ """Glassmorphism card with subtle border glow."""
575
+
576
+ def __init__(self, parent=None):
577
+ super().__init__(parent)
578
+ self.setStyleSheet(f"""
579
+ QFrame {{
580
+ background-color: {C.CARD};
581
+ border: 1px solid {C.GLASS_BORDER};
582
+ border-radius: 12px;
583
+ }}
584
+ """)
585
+ shadow = QGraphicsDropShadowEffect()
586
+ shadow.setBlurRadius(20)
587
+ shadow.setColor(QColor(0, 0, 0, 80))
588
+ shadow.setOffset(0, 4)
589
+ self.setGraphicsEffect(shadow)
590
+
591
+
592
+ class StatCard(GlassCard):
593
+ """Stat card with icon, value, label."""
594
+
595
+ def __init__(self, icon, label, color):
596
+ super().__init__()
597
+ self.setFixedHeight(80)
598
+ self.setMinimumWidth(160)
599
+
600
+ layout = QHBoxLayout(self)
601
+ layout.setContentsMargins(16, 12, 16, 12)
602
+
603
+ # Icon circle
604
+ icon_frame = QLabel(icon)
605
+ icon_frame.setFixedSize(40, 40)
606
+ icon_frame.setAlignment(Qt.AlignmentFlag.AlignCenter)
607
+ icon_frame.setStyleSheet(f"background-color: {color}20; border-radius: 20px; font-size: 18px; border: none;")
608
+ layout.addWidget(icon_frame)
609
+
610
+ # Text
611
+ text_layout = QVBoxLayout()
612
+ text_layout.setSpacing(2)
613
+ self._value = QLabel("---")
614
+ self._value.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold))
615
+ self._value.setStyleSheet(f"color: {color}; border: none;")
616
+ text_layout.addWidget(self._value)
617
+
618
+ lbl = QLabel(label)
619
+ lbl.setStyleSheet(f"color: {C.DIM}; font-size: 11px; border: none;")
620
+ text_layout.addWidget(lbl)
621
+ layout.addLayout(text_layout)
622
+ layout.addStretch()
623
+
624
+ def set_value(self, v):
625
+ self._value.setText(v)
626
+
627
+
628
+ # ============================================================================
629
+ # CUSTOM WIDGETS - RING PROGRESS
630
+ # ============================================================================
631
+
632
+ class RingProgress(QWidget):
633
+ """Animated ring progress indicator."""
634
+
635
+ def __init__(self, size=160, thickness=12):
636
+ super().__init__()
637
+ self.setFixedSize(size, size)
638
+ self._size = size
639
+ self._thickness = thickness
640
+ self._value = 0
641
+ self._target = 0
642
+ self._color_start = C.PINK
643
+ self._color_end = C.GOLD
644
+ self._label = ""
645
+ self._sub = ""
646
+
647
+ self._anim_timer = QTimer(self)
648
+ self._anim_timer.timeout.connect(self._animate)
649
+ self._anim_timer.start(16)
650
+
651
+ def set_value(self, value, label="", sub=""):
652
+ self._target = min(max(value, 0), 100)
653
+ self._label = label
654
+ self._sub = sub
655
+
656
+ def _animate(self):
657
+ if abs(self._value - self._target) > 0.5:
658
+ self._value += (self._target - self._value) * 0.08
659
+ self.update()
660
+
661
+ def paintEvent(self, event):
662
+ p = QPainter(self)
663
+ p.setRenderHint(QPainter.RenderHint.Antialiasing)
664
+
665
+ s = self._size
666
+ cx, cy = s/2, s/2
667
+ r = (s - self._thickness*2) / 2
668
+
669
+ # Background ring
670
+ pen = QPen(QColor(C.MUTED), self._thickness, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap)
671
+ p.setPen(pen)
672
+ p.drawArc(QRectF(cx-r, cy-r, r*2, r*2), 0, 360*16)
673
+
674
+ # Value arc with gradient
675
+ if self._value > 0:
676
+ grad = QConicalGradient(cx, cy, 90)
677
+ grad.setColorAt(0, QColor(self._color_start))
678
+ grad.setColorAt(1, QColor(self._color_end))
679
+ pen = QPen(QBrush(grad), self._thickness, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap)
680
+ p.setPen(pen)
681
+ span = int(self._value / 100 * 360 * 16)
682
+ p.drawArc(QRectF(cx-r, cy-r, r*2, r*2), 90*16, -span)
683
+
684
+ # Center text
685
+ p.setPen(QColor(C.WHITE))
686
+ p.setFont(QFont("Segoe UI", int(s*0.12), QFont.Weight.Bold))
687
+ p.drawText(QRectF(0, cy-s*0.12, s, s*0.2), Qt.AlignmentFlag.AlignCenter, self._label or f"{self._value:.0f}%")
688
+
689
+ p.setPen(QColor(C.DIM))
690
+ p.setFont(QFont("Segoe UI", int(s*0.055)))
691
+ p.drawText(QRectF(0, cy+s*0.05, s, s*0.12), Qt.AlignmentFlag.AlignCenter, self._sub)
692
+
693
+ p.end()
694
+
695
+
696
+ # ============================================================================
697
+ # PAGES
698
+ # ============================================================================
699
+
700
+ class DashboardPage(QWidget):
701
+ log = pyqtSignal(str)
702
+
703
+ def __init__(self):
704
+ super().__init__()
705
+ self.setStyleSheet(f"background: transparent; border: none;")
706
+ layout = QVBoxLayout(self)
707
+ layout.setContentsMargins(24, 24, 24, 24)
708
+ layout.setSpacing(20)
709
+
710
+ # Header
711
+ header = QHBoxLayout()
712
+ greeting = QLabel("Dashboard")
713
+ greeting.setFont(QFont("Segoe UI", 24, QFont.Weight.Bold))
714
+ greeting.setStyleSheet(f"color: {C.WHITE}; border: none;")
715
+ header.addWidget(greeting)
716
+ header.addStretch()
717
+
718
+ quick_btn = QPushButton("Quick Clean")
719
+ quick_btn.setFixedHeight(36)
720
+ quick_btn.setStyleSheet(f"""
721
+ QPushButton {{ background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 {C.PINK}, stop:1 {C.GOLD}); color: white; border: none; border-radius: 18px; padding: 0 24px; font-weight: bold; font-size: 12px; }}
722
+ QPushButton:hover {{ background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 {C.PINK_SOFT}, stop:1 {C.GOLD_BRIGHT}); }}
723
+ """)
724
+ quick_btn.setCursor(Qt.CursorShape.PointingHandCursor)
725
+ quick_btn.clicked.connect(self._quick_clean)
726
+ header.addWidget(quick_btn)
727
+ layout.addLayout(header)
728
+
729
+ # Stats row
730
+ stats = QHBoxLayout()
731
+ stats.setSpacing(16)
732
+ self.card_total = StatCard("💾", "Total Storage", C.CYAN)
733
+ self.card_used = StatCard("📊", "Used Space", C.PINK)
734
+ self.card_free = StatCard("✨", "Free Space", C.GREEN)
735
+ self.card_temp = StatCard("🗑", "Cleanable", C.YELLOW)
736
+ stats.addWidget(self.card_total)
737
+ stats.addWidget(self.card_used)
738
+ stats.addWidget(self.card_free)
739
+ stats.addWidget(self.card_temp)
740
+ layout.addLayout(stats)
741
+
742
+ # Ring + disks
743
+ mid = QHBoxLayout()
744
+ mid.setSpacing(20)
745
+
746
+ # Ring chart
747
+ ring_card = GlassCard()
748
+ ring_layout = QVBoxLayout(ring_card)
749
+ ring_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
750
+ self.ring = RingProgress(180, 14)
751
+ ring_layout.addWidget(self.ring, alignment=Qt.AlignmentFlag.AlignCenter)
752
+ mid.addWidget(ring_card)
753
+
754
+ # Disk table
755
+ table_card = GlassCard()
756
+ tl = QVBoxLayout(table_card)
757
+ tl.setContentsMargins(12, 12, 12, 12)
758
+ tlbl = QLabel("Drives")
759
+ tlbl.setStyleSheet(f"color: {C.DIM}; font-size: 11px; font-weight: bold; border: none;")
760
+ tl.addWidget(tlbl)
761
+ self.table = QTableWidget()
762
+ self.table.setColumnCount(4)
763
+ self.table.setHorizontalHeaderLabels(["Drive","Total","Free","Usage"])
764
+ self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
765
+ self.table.verticalHeader().setVisible(False)
766
+ self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
767
+ self.table.setStyleSheet(f"QTableWidget {{ background: transparent; border: none; color: {C.TEXT}; }} QHeaderView::section {{ background: transparent; color: {C.DIM}; border: none; border-bottom: 1px solid {C.MUTED}; font-size: 10px; }}")
768
+ tl.addWidget(self.table)
769
+ mid.addWidget(table_card)
770
+
771
+ layout.addLayout(mid)
772
+ layout.addStretch()
773
+
774
+ self.refresh()
775
+
776
+ def refresh(self):
777
+ disks = Engine.get_disks()
778
+ self.table.setRowCount(len(disks))
779
+ total_all = sum(d['total'] for d in disks)
780
+ used_all = sum(d['used'] for d in disks)
781
+ free_all = sum(d['free'] for d in disks)
782
+
783
+ for i, d in enumerate(disks):
784
+ for j, val in enumerate([d['drive'], format_size(d['total']), format_size(d['free']), f"{d['pct']}%"]):
785
+ item = QTableWidgetItem(val)
786
+ item.setForeground(QColor(C.TEXT))
787
+ self.table.setItem(i, j, item)
788
+
789
+ self.card_total.set_value(format_size(total_all))
790
+ self.card_used.set_value(format_size(used_all))
791
+ self.card_free.set_value(format_size(free_all))
792
+
793
+ temp_sz, _ = Engine.temp_size()
794
+ self.card_temp.set_value(format_size(temp_sz))
795
+
796
+ pct = round(used_all/total_all*100, 1) if total_all else 0
797
+ self.ring.set_value(pct, f"{pct}%", "Used")
798
+
799
+ def _quick_clean(self):
800
+ r = Engine.clean_temp()
801
+ self.log.emit(f"Quick clean: {r['deleted']} files, {format_size(r['freed'])} freed")
802
+ self.refresh()
803
+
804
+
805
+ class LargeFilesPage(QWidget):
806
+ log = pyqtSignal(str)
807
+
808
+ def __init__(self):
809
+ super().__init__()
810
+ self.setStyleSheet("background: transparent; border: none;")
811
+ layout = QVBoxLayout(self)
812
+ layout.setContentsMargins(24, 24, 24, 24)
813
+ layout.setSpacing(12)
814
+
815
+ title = QLabel("Large Files")
816
+ title.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
817
+ title.setStyleSheet(f"color: {C.WHITE}; border: none;")
818
+ layout.addWidget(title)
819
+
820
+ ctrl = QHBoxLayout()
821
+ self.path_input = QLineEdit(str(Path.home()))
822
+ self.path_input.setStyleSheet(f"background: {C.CARD}; border: 1px solid {C.MUTED}; border-radius: 8px; padding: 8px 12px; color: {C.TEXT};")
823
+ ctrl.addWidget(self.path_input)
824
+
825
+ browse = QPushButton("Browse")
826
+ browse.setStyleSheet(f"background: {C.CARD}; color: {C.TEXT}; border: 1px solid {C.MUTED}; border-radius: 8px; padding: 8px 16px;")
827
+ browse.clicked.connect(lambda: self.path_input.setText(QFileDialog.getExistingDirectory(self) or self.path_input.text()))
828
+ ctrl.addWidget(browse)
829
+
830
+ self.spin = QSpinBox()
831
+ self.spin.setRange(1, 5000)
832
+ self.spin.setValue(50)
833
+ self.spin.setSuffix(" MB min")
834
+ self.spin.setStyleSheet(f"background: {C.CARD}; border: 1px solid {C.MUTED}; border-radius: 8px; padding: 6px; color: {C.TEXT};")
835
+ ctrl.addWidget(self.spin)
836
+ layout.addLayout(ctrl)
837
+
838
+ self.tree = QTreeWidget()
839
+ self.tree.setHeaderLabels(["File","Size","Type","Path"])
840
+ self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
841
+ self.tree.setColumnWidth(1, 90)
842
+ self.tree.setColumnWidth(2, 70)
843
+ self.tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
844
+ self.tree.setAlternatingRowColors(True)
845
+ self.tree.setStyleSheet(f"QTreeWidget {{ background: {C.PANEL}; border: 1px solid {C.MUTED}; border-radius: 8px; color: {C.TEXT}; alternate-background-color: {C.BG2}; }} QTreeWidget::item:selected {{ background: {C.CARD}; }} QHeaderView::section {{ background: {C.BG2}; color: {C.DIM}; border: none; padding: 6px; }}")
846
+ layout.addWidget(self.tree)
847
+
848
+ bottom = QHBoxLayout()
849
+ self.status = QLabel("Ready")
850
+ self.status.setStyleSheet(f"color: {C.DIM}; border: none;")
851
+ bottom.addWidget(self.status)
852
+ bottom.addStretch()
853
+
854
+ scan_btn = QPushButton("Scan")
855
+ scan_btn.setStyleSheet(f"background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 {C.PINK}, stop:1 {C.PURPLE}); color: white; border: none; border-radius: 8px; padding: 10px 24px; font-weight: bold;")
856
+ scan_btn.setCursor(Qt.CursorShape.PointingHandCursor)
857
+ scan_btn.clicked.connect(self._scan)
858
+ bottom.addWidget(scan_btn)
859
+
860
+ del_btn = QPushButton("Delete Selected")
861
+ del_btn.setStyleSheet(f"background: {C.RED}; color: white; border: none; border-radius: 8px; padding: 10px 24px; font-weight: bold;")
862
+ del_btn.setCursor(Qt.CursorShape.PointingHandCursor)
863
+ del_btn.clicked.connect(self._delete)
864
+ bottom.addWidget(del_btn)
865
+ layout.addLayout(bottom)
866
+
867
+ self._worker = None
868
+
869
+ def _scan(self):
870
+ self.tree.clear()
871
+ self.status.setText("Scanning...")
872
+ self._worker = Worker(Engine.scan_large, self.path_input.text(), self.spin.value()*1024*1024)
873
+ self._worker.done.connect(self._done)
874
+ self._worker.start()
875
+
876
+ def _done(self, results):
877
+ colors = {'Video':C.RED,'Audio':C.YELLOW,'Image':C.GREEN,'Archive':C.PURPLE,'Installer':C.PINK,'Document':C.CYAN}
878
+ total = sum(r['size'] for r in results)
879
+ for r in results:
880
+ item = QTreeWidgetItem()
881
+ item.setText(0, r['name'])
882
+ item.setText(1, format_size(r['size']))
883
+ item.setText(2, r['cat'])
884
+ item.setText(3, r['path'])
885
+ item.setData(0, Qt.ItemDataRole.UserRole, r['path'])
886
+ item.setForeground(2, QColor(colors.get(r['cat'], C.DIM)))
887
+ self.tree.addTopLevelItem(item)
888
+ self.status.setText(f"{len(results)} files ({format_size(total)})")
889
+ self.log.emit(f"Scan: {len(results)} large files, {format_size(total)}")
890
+
891
+ def _delete(self):
892
+ items = self.tree.selectedItems()
893
+ if not items: return
894
+ paths = [i.data(0, Qt.ItemDataRole.UserRole) for i in items]
895
+ if QMessageBox.question(self, "Delete", f"Delete {len(paths)} files permanently?") == QMessageBox.StandardButton.Yes:
896
+ r = Engine.delete_files(paths)
897
+ self.log.emit(f"Deleted {r['deleted']}, freed {format_size(r['freed'])}")
898
+ self._scan()
899
+
900
+
901
+ class OldFilesPage(QWidget):
902
+ log = pyqtSignal(str)
903
+
904
+ def __init__(self):
905
+ super().__init__()
906
+ self.setStyleSheet("background: transparent; border: none;")
907
+ layout = QVBoxLayout(self)
908
+ layout.setContentsMargins(24, 24, 24, 24)
909
+ layout.setSpacing(12)
910
+
911
+ title = QLabel("Forgotten Files")
912
+ title.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
913
+ title.setStyleSheet(f"color: {C.WHITE}; border: none;")
914
+ layout.addWidget(title)
915
+
916
+ sub = QLabel("Files you haven't opened in months — probably safe to delete")
917
+ sub.setStyleSheet(f"color: {C.DIM}; border: none;")
918
+ layout.addWidget(sub)
919
+
920
+ ctrl = QHBoxLayout()
921
+ self.path_input = QLineEdit(str(Path.home()))
922
+ self.path_input.setStyleSheet(f"background: {C.CARD}; border: 1px solid {C.MUTED}; border-radius: 8px; padding: 8px; color: {C.TEXT};")
923
+ ctrl.addWidget(self.path_input)
924
+ self.days_spin = QSpinBox()
925
+ self.days_spin.setRange(30, 3650)
926
+ self.days_spin.setValue(180)
927
+ self.days_spin.setSuffix(" days")
928
+ self.days_spin.setStyleSheet(f"background: {C.CARD}; border: 1px solid {C.MUTED}; border-radius: 8px; padding: 6px; color: {C.TEXT};")
929
+ ctrl.addWidget(self.days_spin)
930
+
931
+ scan_btn = QPushButton("Find")
932
+ scan_btn.setStyleSheet(f"background: {C.PINK}; color: white; border: none; border-radius: 8px; padding: 8px 20px; font-weight: bold;")
933
+ scan_btn.clicked.connect(self._scan)
934
+ ctrl.addWidget(scan_btn)
935
+ layout.addLayout(ctrl)
936
+
937
+ self.tree = QTreeWidget()
938
+ self.tree.setHeaderLabels(["File","Size","Last Access","Type"])
939
+ self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
940
+ self.tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
941
+ self.tree.setStyleSheet(f"QTreeWidget {{ background: {C.PANEL}; border: 1px solid {C.MUTED}; border-radius: 8px; color: {C.TEXT}; }} QTreeWidget::item:selected {{ background: {C.CARD}; }} QHeaderView::section {{ background: {C.BG2}; color: {C.DIM}; border: none; padding: 6px; }}")
942
+ layout.addWidget(self.tree)
943
+
944
+ self.status = QLabel("")
945
+ self.status.setStyleSheet(f"color: {C.DIM}; border: none;")
946
+ layout.addWidget(self.status)
947
+ self._worker = None
948
+
949
+ def _scan(self):
950
+ self.tree.clear()
951
+ self._worker = Worker(Engine.scan_old, self.path_input.text(), self.days_spin.value())
952
+ self._worker.done.connect(self._done)
953
+ self._worker.start()
954
+
955
+ def _done(self, results):
956
+ total = sum(r['size'] for r in results)
957
+ for r in results:
958
+ item = QTreeWidgetItem()
959
+ item.setText(0, r['name'])
960
+ item.setText(1, format_size(r['size']))
961
+ item.setText(2, f"{r['days']} days ago")
962
+ item.setText(3, r['cat'])
963
+ item.setData(0, Qt.ItemDataRole.UserRole, r['path'])
964
+ if r['days'] > 365:
965
+ item.setForeground(2, QColor(C.RED))
966
+ elif r['days'] > 180:
967
+ item.setForeground(2, QColor(C.YELLOW))
968
+ self.tree.addTopLevelItem(item)
969
+ self.status.setText(f"{len(results)} forgotten files ({format_size(total)})")
970
+
971
+
972
+ class DuplicatesPage(QWidget):
973
+ log = pyqtSignal(str)
974
+
975
+ def __init__(self):
976
+ super().__init__()
977
+ self.setStyleSheet("background: transparent; border: none;")
978
+ layout = QVBoxLayout(self)
979
+ layout.setContentsMargins(24, 24, 24, 24)
980
+ layout.setSpacing(12)
981
+
982
+ title = QLabel("Duplicates")
983
+ title.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
984
+ title.setStyleSheet(f"color: {C.WHITE}; border: none;")
985
+ layout.addWidget(title)
986
+
987
+ ctrl = QHBoxLayout()
988
+ self.path_input = QLineEdit(str(Path.home()))
989
+ self.path_input.setStyleSheet(f"background: {C.CARD}; border: 1px solid {C.MUTED}; border-radius: 8px; padding: 8px; color: {C.TEXT};")
990
+ ctrl.addWidget(self.path_input)
991
+ scan_btn = QPushButton("Find Duplicates")
992
+ scan_btn.setStyleSheet(f"background: {C.PINK}; color: white; border: none; border-radius: 8px; padding: 8px 20px; font-weight: bold;")
993
+ scan_btn.clicked.connect(self._scan)
994
+ ctrl.addWidget(scan_btn)
995
+ layout.addLayout(ctrl)
996
+
997
+ self.tree = QTreeWidget()
998
+ self.tree.setHeaderLabels(["File","Size","Path"])
999
+ self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
1000
+ self.tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
1001
+ self.tree.setStyleSheet(f"QTreeWidget {{ background: {C.PANEL}; border: 1px solid {C.MUTED}; border-radius: 8px; color: {C.TEXT}; }} QHeaderView::section {{ background: {C.BG2}; color: {C.DIM}; border: none; padding: 6px; }}")
1002
+ layout.addWidget(self.tree)
1003
+
1004
+ self.status = QLabel("")
1005
+ self.status.setStyleSheet(f"color: {C.DIM}; border: none;")
1006
+ layout.addWidget(self.status)
1007
+ self._worker = None
1008
+
1009
+ def _scan(self):
1010
+ self.tree.clear()
1011
+ self.status.setText("Scanning...")
1012
+ self._worker = Worker(Engine.find_dupes, self.path_input.text())
1013
+ self._worker.done.connect(self._done)
1014
+ self._worker.start()
1015
+
1016
+ def _done(self, groups):
1017
+ waste = 0
1018
+ for g in groups[:50]:
1019
+ waste += g['size'] * (len(g['files'])-1)
1020
+ parent = QTreeWidgetItem()
1021
+ parent.setText(0, f"[{len(g['files'])} copies] {os.path.basename(g['files'][0])}")
1022
+ parent.setText(1, format_size(g['size']))
1023
+ parent.setForeground(0, QColor(C.YELLOW))
1024
+ for fp in g['files']:
1025
+ child = QTreeWidgetItem(parent)
1026
+ child.setText(0, os.path.basename(fp))
1027
+ child.setText(1, format_size(g['size']))
1028
+ child.setText(2, fp)
1029
+ child.setData(0, Qt.ItemDataRole.UserRole, fp)
1030
+ self.tree.addTopLevelItem(parent)
1031
+ self.status.setText(f"{len(groups)} groups | Wasted: {format_size(waste)}")
1032
+
1033
+
1034
+ class CleanPage(QWidget):
1035
+ log = pyqtSignal(str)
1036
+
1037
+ def __init__(self):
1038
+ super().__init__()
1039
+ self.setStyleSheet("background: transparent; border: none;")
1040
+ layout = QVBoxLayout(self)
1041
+ layout.setContentsMargins(24, 24, 24, 24)
1042
+ layout.setSpacing(16)
1043
+
1044
+ title = QLabel("System Cleaner")
1045
+ title.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
1046
+ title.setStyleSheet(f"color: {C.WHITE}; border: none;")
1047
+ layout.addWidget(title)
1048
+
1049
+ self.info = QLabel("Analyzing...")
1050
+ self.info.setStyleSheet(f"color: {C.TEXT}; border: none; font-size: 13px;")
1051
+ layout.addWidget(self.info)
1052
+
1053
+ # Options in a glass card
1054
+ opts_card = GlassCard()
1055
+ ol = QVBoxLayout(opts_card)
1056
+ ol.setContentsMargins(16, 16, 16, 16)
1057
+ ol.setSpacing(12)
1058
+
1059
+ for text, checked in [("Temporary Files", True), ("Recycle Bin / Trash", True), ("Browser Cache", False), ("Thumbnail Cache", True)]:
1060
+ cb = QCheckBox(text)
1061
+ cb.setChecked(checked)
1062
+ cb.setStyleSheet(f"QCheckBox {{ color: {C.TEXT}; spacing: 10px; border: none; }} QCheckBox::indicator {{ width: 20px; height: 20px; border: 2px solid {C.MUTED}; border-radius: 5px; background: {C.PANEL}; }} QCheckBox::indicator:checked {{ background: {C.PINK}; border-color: {C.PINK}; }}")
1063
+ ol.addWidget(cb)
1064
+ layout.addWidget(opts_card)
1065
+
1066
+ # Clean button - big gradient
1067
+ clean_btn = QPushButton("CLEAN NOW")
1068
+ clean_btn.setFixedHeight(50)
1069
+ clean_btn.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
1070
+ clean_btn.setCursor(Qt.CursorShape.PointingHandCursor)
1071
+ clean_btn.setStyleSheet(f"""
1072
+ QPushButton {{ background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 {C.PINK}, stop:1 {C.GOLD}); color: white; border: none; border-radius: 25px; }}
1073
+ QPushButton:hover {{ background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 {C.PINK_SOFT}, stop:1 {C.GOLD_BRIGHT}); }}
1074
+ """)
1075
+ clean_btn.clicked.connect(self._clean)
1076
+ layout.addWidget(clean_btn)
1077
+
1078
+ self.result = QLabel("")
1079
+ self.result.setStyleSheet(f"color: {C.GREEN}; border: none; font-size: 14px; font-weight: bold;")
1080
+ layout.addWidget(self.result)
1081
+ layout.addStretch()
1082
+
1083
+ QTimer.singleShot(500, self._analyze)
1084
+
1085
+ def _analyze(self):
1086
+ sz, cnt = Engine.temp_size()
1087
+ self.info.setText(f"Found {cnt:,} cleanable files ({format_size(sz)})")
1088
+
1089
+ def _clean(self):
1090
+ if QMessageBox.question(self, "Clean", "Clean temp files?") == QMessageBox.StandardButton.Yes:
1091
+ r = Engine.clean_temp()
1092
+ self.result.setText(f"Cleaned {r['deleted']:,} files | Freed {format_size(r['freed'])}")
1093
+ self.log.emit(f"Cleaned {r['deleted']} files, freed {format_size(r['freed'])}")
1094
+ QTimer.singleShot(1000, self._analyze)
1095
+
1096
+
1097
+ class LogPage(QWidget):
1098
+ def __init__(self):
1099
+ super().__init__()
1100
+ self.setStyleSheet("background: transparent; border: none;")
1101
+ layout = QVBoxLayout(self)
1102
+ layout.setContentsMargins(24, 24, 24, 24)
1103
+
1104
+ title = QLabel("Activity Log")
1105
+ title.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
1106
+ title.setStyleSheet(f"color: {C.WHITE}; border: none;")
1107
+ layout.addWidget(title)
1108
+
1109
+ self.log_text = QTextEdit()
1110
+ self.log_text.setReadOnly(True)
1111
+ self.log_text.setFont(QFont("JetBrains Mono", 10))
1112
+ self.log_text.setStyleSheet(f"background: {C.PANEL}; border: 1px solid {C.MUTED}; border-radius: 8px; color: {C.TEXT}; padding: 8px;")
1113
+ layout.addWidget(self.log_text)
1114
+
1115
+ self.add("MoneyPackCleaner Ultra started")
1116
+
1117
+ def add(self, msg):
1118
+ ts = datetime.now().strftime("%H:%M:%S")
1119
+ self.log_text.append(f"<span style='color:{C.DIM}'>[{ts}]</span> <span style='color:{C.TEXT}'>{msg}</span>")
1120
+
1121
+
1122
+ # ============================================================================
1123
+ # MAIN WINDOW
1124
+ # ============================================================================
1125
+
1126
+ class MoneyPackCleanerUltra(QMainWindow):
1127
+ def __init__(self):
1128
+ super().__init__()
1129
+ self.setWindowTitle("MoneyPackCleaner")
1130
+ self.setMinimumSize(1100, 700)
1131
+ self.resize(1300, 800)
1132
+
1133
+ # Frameless
1134
+ self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
1135
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, False)
1136
+ self.setStyleSheet(f"QMainWindow {{ background-color: {C.BG}; }}")
1137
+
1138
+ # Main container
1139
+ container = QWidget()
1140
+ self.setCentralWidget(container)
1141
+ main_layout = QVBoxLayout(container)
1142
+ main_layout.setContentsMargins(0, 0, 0, 0)
1143
+ main_layout.setSpacing(0)
1144
+
1145
+ # Title bar
1146
+ self.titlebar = TitleBar(self)
1147
+ main_layout.addWidget(self.titlebar)
1148
+
1149
+ # Body
1150
+ body = QHBoxLayout()
1151
+ body.setContentsMargins(0, 0, 0, 0)
1152
+ body.setSpacing(0)
1153
+
1154
+ # Sidebar
1155
+ self.sidebar = Sidebar()
1156
+ self.sidebar.page_changed.connect(self._change_page)
1157
+ body.addWidget(self.sidebar)
1158
+
1159
+ # Content area with particles
1160
+ content_container = QWidget()
1161
+ content_container.setStyleSheet(f"background-color: {C.BG};")
1162
+ content_layout = QVBoxLayout(content_container)
1163
+ content_layout.setContentsMargins(0, 0, 0, 0)
1164
+
1165
+ # Particles
1166
+ self.particles = ParticleBackground(content_container)
1167
+
1168
+ # Stacked pages
1169
+ self.stack = QStackedWidget()
1170
+ self.stack.setStyleSheet("background: transparent;")
1171
+
1172
+ self.dashboard = DashboardPage()
1173
+ self.large_page = LargeFilesPage()
1174
+ self.old_page = OldFilesPage()
1175
+ self.dup_page = DuplicatesPage()
1176
+ self.clean_page = CleanPage()
1177
+ self.log_page = LogPage()
1178
+
1179
+ self.stack.addWidget(self.dashboard)
1180
+ self.stack.addWidget(self.large_page)
1181
+ self.stack.addWidget(self.old_page)
1182
+ self.stack.addWidget(self.dup_page)
1183
+ self.stack.addWidget(self.clean_page)
1184
+ self.stack.addWidget(self.log_page)
1185
+
1186
+ content_layout.addWidget(self.stack)
1187
+ body.addWidget(content_container)
1188
+
1189
+ main_layout.addLayout(body)
1190
+
1191
+ # Connect logs
1192
+ for page in [self.dashboard, self.large_page, self.old_page, self.dup_page, self.clean_page]:
1193
+ page.log.connect(self.log_page.add)
1194
+
1195
+ # System tray
1196
+ self._setup_tray()
1197
+
1198
+ def _change_page(self, index):
1199
+ self.stack.setCurrentIndex(index)
1200
+
1201
+ def _setup_tray(self):
1202
+ if not QSystemTrayIcon.isSystemTrayAvailable():
1203
+ return
1204
+ pix = QPixmap(32, 32)
1205
+ pix.fill(QColor(0,0,0,0))
1206
+ pa = QPainter(pix)
1207
+ pa.setRenderHint(QPainter.RenderHint.Antialiasing)
1208
+ pa.setBrush(QColor(C.PINK))
1209
+ pa.setPen(Qt.PenStyle.NoPen)
1210
+ pa.drawEllipse(2,2,28,28)
1211
+ pa.setPen(QPen(QColor(C.GOLD),2))
1212
+ pa.setFont(QFont("Arial",14,QFont.Weight.Bold))
1213
+ pa.drawText(QRect(0,0,32,32), Qt.AlignmentFlag.AlignCenter, "M")
1214
+ pa.end()
1215
+
1216
+ self._tray = QSystemTrayIcon(QIcon(pix), self)
1217
+ self._tray.setToolTip("MoneyPackCleaner")
1218
+ menu = QMenu()
1219
+ menu.addAction("Show", self.show)
1220
+ menu.addAction("Quick Clean", self.dashboard._quick_clean)
1221
+ menu.addSeparator()
1222
+ menu.addAction("Exit", QApplication.quit)
1223
+ self._tray.setContextMenu(menu)
1224
+ self._tray.show()
1225
+
1226
+ def resizeEvent(self, event):
1227
+ super().resizeEvent(event)
1228
+ # Resize particles to fill content area
1229
+ if hasattr(self, 'particles'):
1230
+ self.particles.resize(self.width() - 200, self.height() - 42)
1231
+ self.particles.move(200, 42)
1232
+
1233
+ def closeEvent(self, event):
1234
+ if hasattr(self, '_tray') and self._tray.isVisible():
1235
+ self.hide()
1236
+ event.ignore()
1237
+ else:
1238
+ event.accept()
1239
+
1240
+
1241
+ # ============================================================================
1242
+ # ENTRY
1243
+ # ============================================================================
1244
+
1245
+ def main():
1246
+ app = QApplication(sys.argv)
1247
+ app.setFont(QFont("Segoe UI", 10))
1248
+
1249
+ window = MoneyPackCleanerUltra()
1250
+ window.show()
1251
+
1252
+ sys.exit(app.exec())
1253
+
1254
+
1255
+ if __name__ == "__main__":
1256
+ main()