| |
| |
| """ |
| MoneyPack Browser v5.0 - Final |
| Professional dark web browser. Zero white. Zero bugs. |
| Created by MoneyPack. |
| """ |
|
|
| import sys |
| import os |
| import json |
| import urllib.parse |
| from pathlib import Path |
| from datetime import datetime |
|
|
| from PyQt6.QtWidgets import ( |
| QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, |
| QTabWidget, QLabel, QPushButton, QLineEdit, QMenu, |
| QStatusBar, QInputDialog, QMessageBox, QFileDialog, |
| QComboBox, QToolBar |
| ) |
| from PyQt6.QtCore import Qt, QUrl, QTimer, QSize, QRect |
| from PyQt6.QtGui import ( |
| QAction, QKeySequence, QColor, QFont, QIcon, QPixmap, |
| QPainter, QPalette, QShortcut, QLinearGradient, QRadialGradient, |
| QBrush, QPen, QConicalGradient |
| ) |
| from PyQt6.QtWebEngineWidgets import QWebEngineView |
| from PyQt6.QtWebEngineCore import ( |
| QWebEnginePage, QWebEngineProfile, QWebEngineSettings, |
| QWebEngineUrlRequestInterceptor, QWebEngineDownloadRequest, |
| QWebEngineFullScreenRequest |
| ) |
|
|
| APP_NAME = "MoneyPack Browser" |
| APP_VER = "5.0" |
| DATA_DIR = Path.home() / ".mpbrowser" |
| DATA_DIR.mkdir(parents=True, exist_ok=True) |
|
|
| |
| USER_AGENT = ( |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " |
| "AppleWebKit/537.36 (KHTML, like Gecko) " |
| "Chrome/125.0.0.0 Safari/537.36" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| AD_HOSTS = { |
| "doubleclick.net", "googlesyndication.com", "googleadservices.com", |
| "adservice.google.com", "pagead2.googlesyndication.com", |
| "ads.yahoo.com", "ads.twitter.com", "adnxs.com", |
| "outbrain.com", "taboola.com", "criteo.com", "criteo.net", |
| "moatads.com", "quantserve.com", "scorecardresearch.com", |
| "amazon-adsystem.com", "hotjar.com", "popads.net", |
| "propellerads.com", "chartbeat.com", "optimizely.com", |
| "facebook.com/tr", "analytics.google.com", |
| } |
|
|
|
|
| class AdBlockInterceptor(QWebEngineUrlRequestInterceptor): |
| def __init__(self, parent=None): |
| super().__init__(parent) |
| self.blocked_count = 0 |
|
|
| def interceptRequest(self, info): |
| host = info.requestUrl().host() |
| for ad in AD_HOSTS: |
| if ad in host: |
| info.block(True) |
| self.blocked_count += 1 |
| return |
|
|
|
|
| |
| |
| |
|
|
| class BookmarkManager: |
| FILE = DATA_DIR / "bookmarks.json" |
|
|
| @classmethod |
| def load(cls): |
| try: |
| if cls.FILE.exists(): |
| return json.loads(cls.FILE.read_text()) |
| except Exception: |
| pass |
| return [ |
| {"title": "Google", "url": "https://google.com"}, |
| {"title": "YouTube", "url": "https://youtube.com"}, |
| {"title": "GitHub", "url": "https://github.com"}, |
| {"title": "Reddit", "url": "https://reddit.com"}, |
| ] |
|
|
| @classmethod |
| def save(cls, bm): |
| cls.FILE.write_text(json.dumps(bm, indent=2)) |
|
|
| @classmethod |
| def add(cls, title, url): |
| bm = cls.load() |
| bm.append({"title": title, "url": url}) |
| cls.save(bm) |
|
|
|
|
| |
| |
| |
|
|
| NEW_TAB_HTML = """<!DOCTYPE html> |
| <html><head><meta charset="utf-8"><style> |
| html, body { margin:0; padding:0; background:#08080e; color:#888; |
| font-family:system-ui,sans-serif; height:100%; display:flex; |
| align-items:center; justify-content:center; flex-direction:column; } |
| .logo { font-size:32px; font-weight:800; margin-bottom:4px; |
| background:linear-gradient(90deg,#ff1a5c,#d4af37); |
| -webkit-background-clip:text; -webkit-text-fill-color:transparent; } |
| .tag { font-size:9px; letter-spacing:4px; color:#333; margin-bottom:28px; } |
| input { width:460px; max-width:80vw; padding:12px 20px; font-size:14px; |
| background:#0c0c16; border:1px solid #1a1a2a; border-radius:20px; |
| color:#ccc; outline:none; } |
| input:focus { border-color:#ff1a5c; } |
| input::placeholder { color:#333; } |
| .grid { display:grid; grid-template-columns:repeat(4,1fr); gap:8px; |
| margin-top:22px; max-width:480px; } |
| .grid a { background:#0c0c16; border:1px solid #1a1a2a; border-radius:8px; |
| padding:12px 6px; color:#555; text-decoration:none; font-size:10px; |
| font-weight:600; text-align:center; transition:all .2s; } |
| .grid a:hover { border-color:#ff1a5c; color:#ccc; transform:translateY(-2px); } |
| .ft { position:fixed; bottom:8px; font-size:8px; color:#1a1a2a; } |
| </style></head><body> |
| <div class="logo">MoneyPack</div> |
| <div class="tag">BROWSER</div> |
| <form onsubmit="nav(event)"><input id="q" placeholder="Search or type URL" autofocus></form> |
| <div class="grid"> |
| <a href="https://google.com">Google</a> |
| <a href="https://youtube.com">YouTube</a> |
| <a href="https://github.com">GitHub</a> |
| <a href="https://reddit.com">Reddit</a> |
| <a href="https://twitter.com">X</a> |
| <a href="https://twitch.tv">Twitch</a> |
| <a href="https://discord.com">Discord</a> |
| <a href="https://chat.openai.com">ChatGPT</a> |
| </div> |
| <div class="ft">v5.0</div> |
| <script> |
| function nav(e){e.preventDefault();var v=document.getElementById('q').value.trim(); |
| if(!v)return;if(v.indexOf('.')>-1&&v.indexOf(' ')<0&&!v.startsWith('http')) |
| v='https://'+v;else if(!v.startsWith('http')) |
| v='https://google.com/search?q='+encodeURIComponent(v);location.href=v;} |
| </script></body></html>""" |
|
|
|
|
| |
| |
| |
|
|
| class DarkWebPage(QWebEnginePage): |
| """Page with dark background, fullscreen support, popup handling.""" |
| |
| |
| open_in_new_tab = None |
| |
| def __init__(self, profile=None, parent=None): |
| if profile: |
| super().__init__(profile, parent) |
| else: |
| super().__init__(parent) |
| self.setBackgroundColor(QColor("#08080e")) |
| |
| |
| self.fullScreenRequested.connect(self._handle_fullscreen) |
| |
| def _handle_fullscreen(self, request): |
| request.accept() |
| view = self.view() |
| if view: |
| win = view.window() |
| if request.toggleOn(): |
| win.showFullScreen() |
| else: |
| win.showNormal() |
| |
| def createWindow(self, window_type): |
| """Handle popups and target=_blank links.""" |
| |
| new_page = DarkWebPage(self.profile(), self.parent()) |
| |
| if self.open_in_new_tab: |
| self.open_in_new_tab(new_page) |
| return new_page |
| |
| def certificateError(self, error): |
| """Accept cert errors with warning (allows self-signed sites).""" |
| if error.isOverridable(): |
| error.acceptCertificate() |
| return True |
| return False |
|
|
|
|
| |
| |
| |
|
|
| class BrowserTab(QWidget): |
| def __init__(self, profile, open_tab_callback=None, parent=None): |
| super().__init__(parent) |
| layout = QVBoxLayout(self) |
| layout.setContentsMargins(0, 0, 0, 0) |
|
|
| self.webview = QWebEngineView(self) |
| self._page = DarkWebPage(profile, self.webview) |
| self._page.open_in_new_tab = open_tab_callback |
| self.webview.setPage(self._page) |
|
|
| |
| s = self._page.settings() |
| WA = QWebEngineSettings.WebAttribute |
| s.setAttribute(WA.JavascriptEnabled, True) |
| s.setAttribute(WA.PluginsEnabled, True) |
| s.setAttribute(WA.FullScreenSupportEnabled, True) |
| s.setAttribute(WA.ScrollAnimatorEnabled, True) |
| s.setAttribute(WA.Accelerated2dCanvasEnabled, True) |
| s.setAttribute(WA.WebGLEnabled, True) |
| s.setAttribute(WA.LocalStorageEnabled, True) |
| s.setAttribute(WA.JavascriptCanOpenWindows, True) |
| s.setAttribute(WA.DnsPrefetchEnabled, True) |
| s.setAttribute(WA.PdfViewerEnabled, True) |
| s.setAttribute(WA.AutoLoadIconsForPage, True) |
| s.setAttribute(WA.ScreenCaptureEnabled, True) |
|
|
| |
| self._page.linkHovered.connect(self._on_link_hover) |
|
|
| layout.addWidget(self.webview) |
| self._status_callback = None |
|
|
| def set_status_callback(self, fn): |
| self._status_callback = fn |
|
|
| def _on_link_hover(self, url): |
| if self._status_callback: |
| self._status_callback(url if url else "Ready") |
|
|
| def navigate(self, url): |
| if not url.startswith(("http://", "https://", "file://", "about:")): |
| if "." in url and " " not in url: |
| url = "https://" + url |
| else: |
| url = "https://google.com/search?q=" + urllib.parse.quote(url) |
| self.webview.setUrl(QUrl(url)) |
|
|
| def go_home(self): |
| self.webview.setHtml(NEW_TAB_HTML, QUrl("about:home")) |
|
|
|
|
| |
| |
| |
|
|
| class AISidebar(QWidget): |
| SERVICES = [ |
| ("DuckDuckGo AI", "https://duckduckgo.com/?q=hello&ia=chat"), |
| ("HuggingChat", "https://huggingface.co/chat/"), |
| ("Phind", "https://www.phind.com/"), |
| ("You.com", "https://you.com/?chatMode=default"), |
| ("Perplexity", "https://www.perplexity.ai/"), |
| ] |
|
|
| def __init__(self, parent=None): |
| super().__init__(parent) |
| self.setFixedWidth(400) |
| self.setStyleSheet("background:#0a0a14;border-left:1px solid #1a1a2a;") |
|
|
| layout = QVBoxLayout(self) |
| layout.setContentsMargins(0, 0, 0, 0) |
| layout.setSpacing(0) |
|
|
| |
| header = QWidget() |
| header.setFixedHeight(38) |
| header.setStyleSheet("background:#0c0c18;border-bottom:1px solid #1a1a2a;") |
| hl = QHBoxLayout(header) |
| hl.setContentsMargins(10, 0, 10, 0) |
|
|
| lbl = QLabel("AI Assistant") |
| lbl.setFont(QFont("Segoe UI", 10, QFont.Weight.Bold)) |
| lbl.setStyleSheet("color:#d4af37;") |
| hl.addWidget(lbl) |
| hl.addStretch() |
|
|
| self._combo = QComboBox() |
| self._combo.setFixedWidth(130) |
| self._combo.setStyleSheet( |
| "QComboBox{background:#12121e;color:#999;border:1px solid #222;border-radius:4px;padding:3px 6px;font-size:10px;}" |
| ) |
| for name, _ in self.SERVICES: |
| self._combo.addItem(name) |
| self._combo.currentIndexChanged.connect(self._on_switch) |
| hl.addWidget(self._combo) |
|
|
| layout.addWidget(header) |
|
|
| |
| self._webview = QWebEngineView(self) |
| page = DarkWebPage(parent=self._webview) |
| self._webview.setPage(page) |
| self._webview.setUrl(QUrl(self.SERVICES[0][1])) |
| layout.addWidget(self._webview) |
|
|
| def _on_switch(self, idx): |
| if 0 <= idx < len(self.SERVICES): |
| self._webview.setUrl(QUrl(self.SERVICES[idx][1])) |
|
|
|
|
| |
| |
| |
|
|
| STYLESHEET = """ |
| QMainWindow { background: #08080e; } |
| QWidget { background: #08080e; color: #aaa; } |
| QMenuBar { background: #0a0a14; color: #777; border-bottom: 1px solid #1a1a2a; } |
| QMenuBar::item { padding: 5px 12px; } |
| QMenuBar::item:selected { background: #151525; color: #eee; } |
| QMenu { background: #0c0c18; border: 1px solid #1a1a2a; color: #999; } |
| QMenu::item { padding: 6px 22px; } |
| QMenu::item:selected { background: #1a1a30; color: #fff; } |
| QMenu::separator { height: 1px; background: #1a1a2a; margin: 4px 10px; } |
| QTabBar::tab { background: #0a0a14; color: #555; padding: 7px 14px; border: none; |
| border-bottom: 2px solid transparent; font-size: 11px; font-weight: bold; } |
| QTabBar::tab:selected { color: #ff1a5c; border-bottom: 2px solid #ff1a5c; background: #0e0e1a; } |
| QTabBar::tab:hover { color: #aaa; } |
| QTabWidget::pane { border: none; } |
| QLineEdit { background: #0c0c18; border: 1px solid #1a1a2a; border-radius: 16px; |
| padding: 7px 14px; color: #ddd; font-size: 12px; } |
| QLineEdit:focus { border-color: #ff1a5c; } |
| QPushButton { background: transparent; color: #555; border: none; padding: 4px 8px; |
| border-radius: 4px; font-size: 14px; } |
| QPushButton:hover { background: #151525; color: #eee; } |
| QStatusBar { background: #0a0a14; color: #333; border-top: 1px solid #1a1a2a; font-size: 10px; } |
| QComboBox { background: #0c0c18; color: #999; border: 1px solid #1a1a2a; border-radius: 4px; padding: 3px 8px; } |
| QToolTip { background: #0c0c18; color: #ccc; border: 1px solid #ff1a5c; padding: 4px; } |
| """ |
|
|
|
|
| class MoneyPackBrowser(QMainWindow): |
| def __init__(self): |
| super().__init__() |
| self.setWindowTitle(APP_NAME) |
| self.resize(1350, 820) |
| self.setMinimumSize(900, 600) |
| self.setStyleSheet(STYLESHEET) |
|
|
| |
| self._profile = QWebEngineProfile.defaultProfile() |
| self._profile.setHttpUserAgent(USER_AGENT) |
| self._profile.setPersistentStoragePath(str(DATA_DIR / "storage")) |
| self._profile.setCachePath(str(DATA_DIR / "cache")) |
| self._profile.setPersistentCookiesPolicy( |
| QWebEngineProfile.PersistentCookiesPolicy.AllowPersistentCookies |
| ) |
|
|
| |
| self._ad_blocker = AdBlockInterceptor(self) |
| self._profile.setUrlRequestInterceptor(self._ad_blocker) |
|
|
| |
| self._downloads = [] |
| self._profile.downloadRequested.connect(self._on_download) |
|
|
| self._private_mode = False |
| self._ai_visible = False |
|
|
| self._create_menubar() |
| self._create_ui() |
| self._create_shortcuts() |
|
|
| |
| self._new_tab() |
|
|
| |
| def _create_menubar(self): |
| mb = self.menuBar() |
|
|
| |
| file_menu = mb.addMenu("File") |
| self._add_action(file_menu, "New Tab", self._new_tab, "Ctrl+T") |
| self._add_action(file_menu, "Close Tab", self._close_current_tab, "Ctrl+W") |
| file_menu.addSeparator() |
| self._add_action(file_menu, "Private Mode", self._toggle_private) |
| file_menu.addSeparator() |
| self._add_action(file_menu, "Quit", self.close, "Ctrl+Q") |
|
|
| |
| nav_menu = mb.addMenu("Navigate") |
| self._add_action(nav_menu, "Back", self._back, "Alt+Left") |
| self._add_action(nav_menu, "Forward", self._forward, "Alt+Right") |
| self._add_action(nav_menu, "Reload", self._reload, "F5") |
| self._add_action(nav_menu, "Home", self._home) |
|
|
| |
| view_menu = mb.addMenu("View") |
| self._add_action(view_menu, "Zoom In", self._zoom_in, "Ctrl++") |
| self._add_action(view_menu, "Zoom Out", self._zoom_out, "Ctrl+-") |
| self._add_action(view_menu, "Reset Zoom", self._zoom_reset, "Ctrl+0") |
| view_menu.addSeparator() |
| self._add_action(view_menu, "Full Screen", self._toggle_fullscreen, "F11") |
| self._add_action(view_menu, "Find in Page", self._find, "Ctrl+F") |
|
|
| |
| tools_menu = mb.addMenu("Tools") |
| self._add_action(tools_menu, "AI Sidebar", self._toggle_ai, "Ctrl+Shift+A") |
| self._add_action(tools_menu, "Command Palette", self._command_palette, "Ctrl+K") |
| self._add_action(tools_menu, "Split View", self._split_view, "Ctrl+Shift+S") |
| tools_menu.addSeparator() |
| self._add_action(tools_menu, "Reader Mode", self._reader_mode, "Ctrl+Shift+R") |
| self._add_action(tools_menu, "Force Dark Mode", self._force_dark) |
| self._add_action(tools_menu, "Focus Mode", self._focus_mode) |
| self._add_action(tools_menu, "Screenshot", self._screenshot, "Ctrl+Shift+X") |
| self._add_action(tools_menu, "Notes", self._notes_panel, "Ctrl+Shift+N") |
| tools_menu.addSeparator() |
| self._add_action(tools_menu, "Force Max Quality", self._video_max_quality, "Ctrl+Shift+Q") |
| self._add_action(tools_menu, "Picture in Picture", self._video_pip, "Ctrl+Shift+P") |
| self._add_action(tools_menu, "Theater Mode", self._video_theater) |
| self._add_action(tools_menu, "Enhance Video", self._video_enhance, "Ctrl+Shift+E") |
| tools_menu.addSeparator() |
| self._add_action(tools_menu, "Bookmark Page", self._bookmark, "Ctrl+D") |
| |
| |
| power_menu = mb.addMenu("Power") |
| self._add_action(power_menu, "New Identity (wipe all)", self._new_identity) |
| self._add_action(power_menu, "Workspaces", self._switch_workspace, "Ctrl+Shift+W") |
| self._add_action(power_menu, "Save Session", self._save_session) |
| self._add_action(power_menu, "Suspend Tab", self._suspend_tab) |
| self._add_action(power_menu, "Web App Mode", self._web_app_mode) |
| power_menu.addSeparator() |
| self._add_action(power_menu, "Auto-Fill Form", self._auto_fill, "Ctrl+Shift+F") |
| self._add_action(power_menu, "Turbo Mode (strip media)", self._turbo_mode) |
| self._add_action(power_menu, "Block Fingerprinting", self._block_fingerprint) |
| self._add_action(power_menu, "Quick Actions", self._gesture_menu, "Ctrl+G") |
| power_menu.addSeparator() |
| self._add_action(power_menu, "FIRE Button (nuke all)", self._fire_button) |
| self._add_action(power_menu, "Compact Mode", self._toggle_compact, "Ctrl+Shift+H") |
| self._add_action(power_menu, "Video Assistant", self._video_assistant) |
| self._add_action(power_menu, "Load Userscript", self._load_userscript) |
| self._add_action(power_menu, "Page Info", self._page_info) |
| self._add_action(power_menu, "View Source", self._view_source, "Ctrl+U") |
| self._add_action(power_menu, "File Browser", self._file_browser) |
| self._add_action(power_menu, "Link Hints (click by number)", self._link_hints, "Ctrl+.") |
| self._add_action(power_menu, "History", self._history_view, "Ctrl+H") |
| |
| |
| extra_menu = mb.addMenu("Extra") |
| self._add_action(extra_menu, "URL Notes (per-site)", self._url_notes) |
| self._add_action(extra_menu, "Force HTTPS", self._force_https) |
| self._add_action(extra_menu, "Connection Monitor", self._connection_monitor) |
| self._add_action(extra_menu, "SSL Check", self._ssl_check) |
| self._add_action(extra_menu, "Annotate Page", self._annotate_page) |
| self._add_action(extra_menu, "Quick Launch Bar", self._quick_launch, "Ctrl+Space") |
| self._add_action(extra_menu, "Custom Block Rules", self._custom_block_rules) |
| self._add_action(extra_menu, "Inject Custom CSS", self._inject_css) |
| self._add_action(extra_menu, "Tab Auto-Expire Timer", self._set_tab_expire) |
| self._add_action(extra_menu, "Isolated Tab (separate session)", self._isolated_tab) |
| extra_menu.addSeparator() |
| self._add_action(extra_menu, "Responsive Preview", self._responsive_preview) |
| self._add_action(extra_menu, "Emulate Device", self._emulate_device) |
| self._add_action(extra_menu, "Page Load Time", self._page_timer) |
| self._add_action(extra_menu, "Tab Overview Grid", self._tab_overview, "Ctrl+Shift+G") |
| self._add_action(extra_menu, "Clipboard History", self._clipboard_history) |
| self._add_action(extra_menu, "Read Page Aloud", self._read_aloud) |
| self._add_action(extra_menu, "Share via QR Code", self._share_qr) |
| self._add_action(extra_menu, "Session Stats", self._productivity_stats) |
| self._add_action(extra_menu, "Scroll to Top", self._scroll_top, "Home") |
| self._add_action(extra_menu, "Scroll to Bottom", self._scroll_bottom, "End") |
| |
| |
| final_menu = mb.addMenu("More") |
| self._add_action(final_menu, "Pin/Unpin Tab", self._pin_tab) |
| self._add_action(final_menu, "Mute Tab", self._mute_tab, "Ctrl+M") |
| self._add_action(final_menu, "Page Performance Score", self._page_performance) |
| self._add_action(final_menu, "Save for Later", self._save_for_later, "Ctrl+Shift+L") |
| self._add_action(final_menu, "Reading List", self._reading_list) |
| self._add_action(final_menu, "Toggle JavaScript", self._toggle_javascript) |
| self._add_action(final_menu, "Search Tabs", self._search_tabs, "Ctrl+Shift+Tab") |
| self._add_action(final_menu, "Temp Email Generator", self._generate_temp_email) |
| self._add_action(final_menu, "Color Picker", self._color_picker) |
| self._add_action(final_menu, "Narrow Mode", self._narrow_mode) |
| self._add_action(final_menu, "Save Zoom for Site", self._save_zoom) |
| self._add_action(final_menu, "Session Info", self._show_session_info) |
|
|
| |
| bm_menu = mb.addMenu("Bookmarks") |
| for bm in BookmarkManager.load(): |
| self._add_action(bm_menu, bm["title"], lambda checked=False, u=bm["url"]: self._new_tab(u)) |
|
|
| |
| help_menu = mb.addMenu("Help") |
| self._add_action(help_menu, "About", self._about) |
|
|
| def _add_action(self, menu, text, callback, shortcut=None): |
| """Helper: properly create QAction for PyQt6 compatibility.""" |
| action = QAction(text, self) |
| if shortcut: |
| action.setShortcut(QKeySequence(shortcut)) |
| action.triggered.connect(callback) |
| menu.addAction(action) |
| return action |
|
|
| |
| def _create_ui(self): |
| central = QWidget() |
| self.setCentralWidget(central) |
| main_layout = QHBoxLayout(central) |
| main_layout.setContentsMargins(0, 0, 0, 0) |
| main_layout.setSpacing(0) |
|
|
| |
| browser_area = QWidget() |
| browser_layout = QVBoxLayout(browser_area) |
| browser_layout.setContentsMargins(0, 0, 0, 0) |
| browser_layout.setSpacing(0) |
|
|
| |
| toolbar = QWidget() |
| toolbar.setFixedHeight(42) |
| toolbar.setStyleSheet("background:#0a0a14;border-bottom:1px solid #1a1a2a;") |
| tl = QHBoxLayout(toolbar) |
| tl.setContentsMargins(8, 4, 8, 4) |
| tl.setSpacing(4) |
|
|
| |
| for symbol, fn in [("\u2190", self._back), ("\u2192", self._forward), |
| ("\u21bb", self._reload), ("\u2302", self._home)]: |
| btn = QPushButton(symbol) |
| btn.setFixedSize(30, 30) |
| btn.clicked.connect(fn) |
| tl.addWidget(btn) |
|
|
| |
| self._url_bar = QLineEdit() |
| self._url_bar.setPlaceholderText("Search or enter URL...") |
| self._url_bar.returnPressed.connect(self._navigate_url) |
| tl.addWidget(self._url_bar) |
|
|
| |
| bm_btn = QPushButton("\u2606") |
| bm_btn.setToolTip("Bookmark (Ctrl+D)") |
| bm_btn.clicked.connect(self._bookmark) |
| tl.addWidget(bm_btn) |
|
|
| |
| ai_btn = QPushButton("AI") |
| ai_btn.setToolTip("AI Sidebar (Ctrl+Shift+A)") |
| ai_btn.setStyleSheet( |
| "QPushButton{background:#1a1a2a;color:#d4af37;border-radius:4px;" |
| "font-size:10px;font-weight:bold;padding:4px 10px}" |
| "QPushButton:hover{background:#ff1a5c;color:#fff}" |
| ) |
| ai_btn.clicked.connect(self._toggle_ai) |
| tl.addWidget(ai_btn) |
|
|
| |
| self._ad_label = QLabel("0") |
| self._ad_label.setToolTip("Ads blocked") |
| self._ad_label.setStyleSheet("color:#00e676;font-size:9px;font-weight:bold;padding:0 6px;") |
| tl.addWidget(self._ad_label) |
|
|
| browser_layout.addWidget(toolbar) |
|
|
| |
| self._tabs = QTabWidget() |
| self._tabs.setTabsClosable(True) |
| self._tabs.setMovable(True) |
| self._tabs.setDocumentMode(True) |
| self._tabs.tabCloseRequested.connect(self._close_tab) |
| self._tabs.currentChanged.connect(self._on_tab_changed) |
| browser_layout.addWidget(self._tabs) |
|
|
| main_layout.addWidget(browser_area) |
|
|
| |
| self._ai_sidebar = AISidebar(self) |
| self._ai_sidebar.setVisible(False) |
| main_layout.addWidget(self._ai_sidebar) |
|
|
| |
| self.statusBar().showMessage("Ready") |
|
|
| |
| timer = QTimer(self) |
| timer.timeout.connect(lambda: self._ad_label.setText(str(self._ad_blocker.blocked_count))) |
| timer.start(3000) |
|
|
| def _create_shortcuts(self): |
| QShortcut(QKeySequence("Ctrl+L"), self, lambda: (self._url_bar.selectAll(), self._url_bar.setFocus())) |
| QShortcut(QKeySequence("Ctrl+R"), self, self._reload) |
| for i in range(9): |
| QShortcut(QKeySequence(f"Ctrl+{i+1}"), self, |
| lambda idx=i: self._tabs.setCurrentIndex(idx) if idx < self._tabs.count() else None) |
|
|
| |
| def _new_tab(self, url=None): |
| |
| if self._private_mode: |
| profile = QWebEngineProfile(self) |
| profile.setHttpUserAgent(USER_AGENT) |
| else: |
| profile = self._profile |
|
|
| tab = BrowserTab(profile, open_tab_callback=self._open_popup_tab, parent=self) |
| tab.set_status_callback(lambda msg: self.statusBar().showMessage(msg)) |
|
|
| if url: |
| tab.navigate(url) |
| else: |
| tab.go_home() |
|
|
| |
| tab.webview.titleChanged.connect(lambda title, t=tab: self._update_tab_title(t, title)) |
| tab.webview.urlChanged.connect(lambda qurl, t=tab: self._update_url_bar(t, qurl)) |
| tab.webview.loadStarted.connect(lambda: self.statusBar().showMessage("Loading...")) |
| tab.webview.loadFinished.connect(lambda ok: self.statusBar().showMessage("Done" if ok else "Error")) |
| tab.webview.iconChanged.connect(lambda icon, t=tab: self._update_tab_icon(t, icon)) |
|
|
| idx = self._tabs.addTab(tab, "New Tab") |
| self._tabs.setCurrentIndex(idx) |
| self._url_bar.clear() |
| self._url_bar.setFocus() |
|
|
| def _open_popup_tab(self, new_page): |
| """Called when a page creates a popup/new window.""" |
| tab = BrowserTab(self._profile, open_tab_callback=self._open_popup_tab, parent=self) |
| |
| tab.webview.setPage(new_page) |
| tab.set_status_callback(lambda msg: self.statusBar().showMessage(msg)) |
| |
| tab.webview.titleChanged.connect(lambda title, t=tab: self._update_tab_title(t, title)) |
| tab.webview.urlChanged.connect(lambda qurl, t=tab: self._update_url_bar(t, qurl)) |
| tab.webview.iconChanged.connect(lambda icon, t=tab: self._update_tab_icon(t, icon)) |
| |
| idx = self._tabs.addTab(tab, "Loading...") |
| self._tabs.setCurrentIndex(idx) |
|
|
| def _on_download(self, download): |
| """Handle file downloads.""" |
| from PyQt6.QtCore import QStandardPaths |
| default_path = os.path.join( |
| QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DownloadLocation), |
| download.suggestedFileName() |
| ) |
| save_path, _ = QFileDialog.getSaveFileName(self, "Save File", default_path) |
| if not save_path: |
| download.cancel() |
| return |
| |
| download.setDownloadDirectory(os.path.dirname(save_path)) |
| download.setDownloadFileName(os.path.basename(save_path)) |
| download.accept() |
| |
| self._downloads.append(download) |
| download.receivedBytesChanged.connect(lambda: self._download_progress(download)) |
| download.isFinishedChanged.connect(lambda: self._download_finished(download)) |
| self.statusBar().showMessage(f"Downloading: {os.path.basename(save_path)}") |
|
|
| def _download_progress(self, download): |
| if download.totalBytes() > 0: |
| pct = int(download.receivedBytes() / download.totalBytes() * 100) |
| self.statusBar().showMessage(f"Downloading: {pct}% - {download.downloadFileName()}") |
|
|
| def _download_finished(self, download): |
| if download.isFinished(): |
| self.statusBar().showMessage(f"Download complete: {download.downloadFileName()}") |
| if download in self._downloads: |
| self._downloads.remove(download) |
|
|
| def _close_tab(self, idx): |
| if self._tabs.count() <= 1: |
| self.close() |
| return |
| widget = self._tabs.widget(idx) |
| self._tabs.removeTab(idx) |
| widget.deleteLater() |
|
|
| def _close_current_tab(self): |
| self._close_tab(self._tabs.currentIndex()) |
|
|
| def _on_tab_changed(self, idx): |
| tab = self._tabs.widget(idx) |
| if tab: |
| url = tab.webview.url().toString() |
| if url and not url.startswith("about:"): |
| self._url_bar.setText(url) |
|
|
| def _update_tab_title(self, tab, title): |
| idx = self._tabs.indexOf(tab) |
| if idx >= 0: |
| display = (title[:22] + "..") if len(title) > 22 else title |
| self._tabs.setTabText(idx, display or "New Tab") |
|
|
| def _update_tab_icon(self, tab, icon): |
| idx = self._tabs.indexOf(tab) |
| if idx >= 0 and not icon.isNull(): |
| self._tabs.setTabIcon(idx, icon) |
|
|
| def _update_url_bar(self, tab, qurl): |
| if tab == self._tabs.currentWidget(): |
| url = qurl.toString() |
| if url and not url.startswith(("about:", "data:")): |
| self._url_bar.setText(url) |
|
|
| |
| def _navigate_url(self): |
| url = self._url_bar.text().strip() |
| if url: |
| tab = self._tabs.currentWidget() |
| if tab: |
| tab.navigate(url) |
|
|
| def _current_web(self): |
| tab = self._tabs.currentWidget() |
| return tab.webview if tab else None |
|
|
| def _back(self): |
| w = self._current_web() |
| if w: w.back() |
|
|
| def _forward(self): |
| w = self._current_web() |
| if w: w.forward() |
|
|
| def _reload(self): |
| w = self._current_web() |
| if w: w.reload() |
|
|
| def _home(self): |
| tab = self._tabs.currentWidget() |
| if tab: tab.go_home() |
|
|
| |
| def _zoom_in(self): |
| w = self._current_web() |
| if w: w.setZoomFactor(w.zoomFactor() + 0.1) |
|
|
| def _zoom_out(self): |
| w = self._current_web() |
| if w: w.setZoomFactor(max(0.25, w.zoomFactor() - 0.1)) |
|
|
| def _zoom_reset(self): |
| w = self._current_web() |
| if w: w.setZoomFactor(1.0) |
|
|
| def _toggle_fullscreen(self): |
| if self.isFullScreen(): |
| self.showNormal() |
| else: |
| self.showFullScreen() |
|
|
| def _find(self): |
| text, ok = QInputDialog.getText(self, "Find", "Search for:") |
| if ok and text: |
| w = self._current_web() |
| if w: w.findText(text) |
|
|
| |
| def _toggle_ai(self): |
| self._ai_visible = not self._ai_visible |
| self._ai_sidebar.setVisible(self._ai_visible) |
|
|
| def _toggle_private(self): |
| self._private_mode = not self._private_mode |
| mode = "ON" if self._private_mode else "OFF" |
| self.statusBar().showMessage(f"Private Mode {mode}") |
|
|
| def _bookmark(self): |
| tab = self._tabs.currentWidget() |
| if tab: |
| title = tab.webview.title() |
| url = tab.webview.url().toString() |
| if url and not url.startswith("about:"): |
| BookmarkManager.add(title, url) |
| self.statusBar().showMessage(f"Bookmarked: {title}") |
|
|
| |
| def _run_js(self, code): |
| w = self._current_web() |
| if w: w.page().runJavaScript(code) |
|
|
| def _video_max_quality(self): |
| self._run_js( |
| "(function(){var p=document.getElementById('movie_player');" |
| "if(p&&p.getAvailableQualityLevels){" |
| "var l=p.getAvailableQualityLevels();" |
| "if(l.length>0)p.setPlaybackQualityRange(l[0],l[0])}" |
| "})()" |
| ) |
| self.statusBar().showMessage("Forced max video quality") |
|
|
| def _video_pip(self): |
| self._run_js( |
| "(function(){var v=document.querySelector('video');" |
| "if(v){if(document.pictureInPictureElement)" |
| "document.exitPictureInPicture();" |
| "else v.requestPictureInPicture()}})()" |
| ) |
|
|
| def _video_theater(self): |
| self._run_js( |
| "(function(){var e=document.getElementById('mp-theater');" |
| "if(e){e.remove();var v=document.querySelector('video');" |
| "if(v)v.style.cssText='';return}" |
| "var v=document.querySelector('video');if(!v)return;" |
| "var o=document.createElement('div');o.id='mp-theater';" |
| "o.style.cssText='position:fixed;top:0;left:0;width:100%;" |
| "height:100%;background:rgba(0,0,0,.95);z-index:99998;cursor:pointer';" |
| "o.onclick=function(){this.remove();v.style.cssText=''};" |
| "document.body.appendChild(o);" |
| "v.style.cssText='position:fixed;top:50%;left:50%;" |
| "transform:translate(-50%,-50%);width:85vw;max-height:85vh;" |
| "z-index:99999;border-radius:8px'})()" |
| ) |
|
|
| def _video_enhance(self): |
| self._run_js( |
| "(function(){var v=document.querySelector('video');" |
| "if(!v)return;if(v.style.filter)v.style.filter='';" |
| "else v.style.filter='contrast(1.06) saturate(1.2) brightness(1.02)'})()" |
| ) |
| self.statusBar().showMessage("Video enhancement toggled") |
|
|
| |
| def _split_view(self): |
| """Split current tab with another side-by-side.""" |
| if self._tabs.count() < 2: |
| self.statusBar().showMessage("Need 2+ tabs to split") |
| return |
| |
| |
| current_idx = self._tabs.currentIndex() |
| other_idx = (current_idx + 1) % self._tabs.count() |
| |
| current_tab = self._tabs.widget(current_idx) |
| other_tab = self._tabs.widget(other_idx) |
| |
| if not current_tab or not other_tab: |
| return |
| |
| |
| from PyQt6.QtWidgets import QSplitter |
| split_widget = QWidget() |
| split_layout = QHBoxLayout(split_widget) |
| split_layout.setContentsMargins(0, 0, 0, 0) |
| |
| splitter = QSplitter(Qt.Orientation.Horizontal) |
| |
| |
| left = QWebEngineView() |
| left_page = DarkWebPage(self._profile, left) |
| left.setPage(left_page) |
| left.setUrl(current_tab.webview.url()) |
| |
| right = QWebEngineView() |
| right_page = DarkWebPage(self._profile, right) |
| right.setPage(right_page) |
| right.setUrl(other_tab.webview.url()) |
| |
| splitter.addWidget(left) |
| splitter.addWidget(right) |
| split_layout.addWidget(splitter) |
| |
| |
| idx = self._tabs.addTab(split_widget, "Split View") |
| self._tabs.setCurrentIndex(idx) |
| self.statusBar().showMessage("Split view opened") |
|
|
| |
| def _command_palette(self): |
| """Quick launcher - search tabs, bookmarks, actions.""" |
| from PyQt6.QtWidgets import QDialog, QListWidget, QListWidgetItem |
| |
| dialog = QDialog(self) |
| dialog.setWindowTitle("Command Palette") |
| dialog.setFixedSize(500, 400) |
| dialog.setStyleSheet(f"QDialog{{background:#0c0c18;border:1px solid #ff1a5c;border-radius:12px;}}" |
| f"QLineEdit{{background:#08080e;border:1px solid #1a1a2a;border-radius:8px;padding:10px;color:#ddd;font-size:14px;}}" |
| f"QLineEdit:focus{{border-color:#ff1a5c;}}" |
| f"QListWidget{{background:#08080e;border:none;color:#aaa;font-size:12px;}}" |
| f"QListWidget::item{{padding:8px;}}" |
| f"QListWidget::item:selected{{background:#1a1a30;color:#fff;}}") |
| |
| layout = QVBoxLayout(dialog) |
| layout.setContentsMargins(12, 12, 12, 12) |
| |
| search = QLineEdit() |
| search.setPlaceholderText("Search tabs, bookmarks, actions...") |
| layout.addWidget(search) |
| |
| results = QListWidget() |
| layout.addWidget(results) |
| |
| |
| all_items = [] |
| |
| |
| for i in range(self._tabs.count()): |
| tab = self._tabs.widget(i) |
| if hasattr(tab, 'webview'): |
| title = tab.webview.title() or "New Tab" |
| url = tab.webview.url().toString() |
| all_items.append(("tab", f"Tab: {title}", url, i)) |
| |
| |
| for bm in BookmarkManager.load(): |
| all_items.append(("bookmark", f"Bookmark: {bm['title']}", bm['url'], None)) |
| |
| |
| actions = [ |
| ("action", "New Tab", "new_tab", None), |
| ("action", "Private Mode", "private", None), |
| ("action", "AI Sidebar", "ai", None), |
| ("action", "Split View", "split", None), |
| ("action", "Reader Mode", "reader", None), |
| ("action", "Screenshot", "screenshot", None), |
| ("action", "Clear History", "clear", None), |
| ("action", "Force Dark Mode", "darkmode", None), |
| ] |
| all_items.extend(actions) |
| |
| def filter_items(text): |
| results.clear() |
| text = text.lower() |
| for item_type, label, data, extra in all_items: |
| if not text or text in label.lower() or text in str(data).lower(): |
| li = QListWidgetItem(label) |
| li.setData(Qt.ItemDataRole.UserRole, (item_type, data, extra)) |
| results.addItem(li) |
| |
| def execute_item(item): |
| if not item: |
| return |
| item_type, data, extra = item.data(Qt.ItemDataRole.UserRole) |
| dialog.close() |
| |
| if item_type == "tab": |
| self._tabs.setCurrentIndex(extra) |
| elif item_type == "bookmark": |
| self._new_tab(data) |
| elif item_type == "action": |
| if data == "new_tab": self._new_tab() |
| elif data == "private": self._toggle_private() |
| elif data == "ai": self._toggle_ai() |
| elif data == "split": self._split_view() |
| elif data == "reader": self._reader_mode() |
| elif data == "screenshot": self._screenshot() |
| elif data == "darkmode": self._force_dark() |
| |
| search.textChanged.connect(filter_items) |
| results.itemDoubleClicked.connect(execute_item) |
| results.itemActivated.connect(execute_item) |
| |
| filter_items("") |
| dialog.exec() |
|
|
| |
| def _reader_mode(self): |
| """Strip page to clean readable text.""" |
| self._run_js(""" |
| (function(){ |
| if(document.getElementById('mp-reader')){document.getElementById('mp-reader').remove();document.body.style.display='';return} |
| var article = document.querySelector('article') || document.querySelector('[role=main]') || document.querySelector('.content') || document.querySelector('#content') || document.body; |
| var text = article.innerHTML; |
| var title = document.title; |
| document.body.style.display='none'; |
| var reader = document.createElement('div'); |
| reader.id = 'mp-reader'; |
| reader.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:#0c0c16;color:#ccc;overflow-y:auto;z-index:999999;padding:40px;font-family:Georgia,serif;font-size:18px;line-height:1.8;'; |
| reader.innerHTML = '<div style="max-width:700px;margin:0 auto"><h1 style="color:#d4af37;font-size:28px;margin-bottom:20px">'+title+'</h1>'+text+'<br><br><button onclick="this.parentElement.parentElement.remove();document.body.style.display=\\'\\';" style="background:#ff1a5c;color:white;border:none;padding:10px 20px;border-radius:8px;cursor:pointer;font-size:14px">Exit Reader Mode</button></div>'; |
| document.documentElement.appendChild(reader); |
| })() |
| """) |
| self.statusBar().showMessage("Reader mode toggled") |
|
|
| |
| def _screenshot(self): |
| """Take screenshot of current page.""" |
| w = self._current_web() |
| if not w: |
| return |
| |
| |
| from PyQt6.QtCore import QStandardPaths |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| default_path = os.path.join( |
| QStandardPaths.writableLocation(QStandardPaths.StandardLocation.PicturesLocation), |
| f"moneypack_screenshot_{timestamp}.png" |
| ) |
| |
| save_path, _ = QFileDialog.getSaveFileName(self, "Save Screenshot", default_path, "PNG (*.png)") |
| if save_path: |
| |
| pixmap = w.grab() |
| pixmap.save(save_path) |
| self.statusBar().showMessage(f"Screenshot saved: {os.path.basename(save_path)}") |
|
|
| |
| def _force_dark(self): |
| """Inject dark mode CSS into current page.""" |
| self._run_js(""" |
| (function(){ |
| var id = 'mp-darkmode'; |
| var existing = document.getElementById(id); |
| if(existing){existing.remove();return} |
| var style = document.createElement('style'); |
| style.id = id; |
| style.textContent = 'html{filter:invert(0.88) hue-rotate(180deg)!important}img,video,canvas,svg,[style*=background-image]{filter:invert(1) hue-rotate(180deg)!important}'; |
| document.head.appendChild(style); |
| })() |
| """) |
| self.statusBar().showMessage("Dark mode toggled on page") |
|
|
| |
| def _notes_panel(self): |
| """Quick scratchpad/notes.""" |
| from PyQt6.QtWidgets import QDialog, QPlainTextEdit |
| |
| dialog = QDialog(self) |
| dialog.setWindowTitle("Quick Notes") |
| dialog.setFixedSize(400, 300) |
| dialog.setStyleSheet("QDialog{background:#0c0c18;}QPlainTextEdit{background:#08080e;color:#ccc;border:1px solid #1a1a2a;border-radius:8px;padding:10px;font-size:13px;}") |
| |
| layout = QVBoxLayout(dialog) |
| |
| notes_file = DATA_DIR / "notes.txt" |
| text_edit = QPlainTextEdit() |
| text_edit.setPlaceholderText("Type your notes here...") |
| |
| |
| if notes_file.exists(): |
| text_edit.setPlainText(notes_file.read_text()) |
| |
| layout.addWidget(text_edit) |
| |
| |
| def save_notes(): |
| notes_file.write_text(text_edit.toPlainText()) |
| |
| dialog.finished.connect(lambda: save_notes()) |
| dialog.exec() |
|
|
| |
| def _focus_mode(self): |
| """Block distracting sites.""" |
| blocked = ["facebook.com", "twitter.com", "instagram.com", "tiktok.com", "reddit.com", "youtube.com"] |
| w = self._current_web() |
| if w: |
| current_host = w.url().host() |
| for site in blocked: |
| if site in current_host: |
| w.setHtml(f"""<!DOCTYPE html><html><head><style> |
| body{{background:#08080e;color:#ff1a5c;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;text-align:center}} |
| h1{{font-size:48px}}p{{color:#666;margin-top:10px}} |
| </style></head><body><div><h1>BLOCKED</h1><p>Focus Mode is active.<br>{current_host} is blocked.</p><p style="color:#333;margin-top:30px">MoneyPack Browser - Stay focused.</p></div></body></html>""") |
| self.statusBar().showMessage(f"Blocked: {current_host} (Focus Mode)") |
| return |
| self.statusBar().showMessage("This site is not in the block list") |
|
|
| |
| |
| |
|
|
| |
| def _new_identity(self): |
| """Wipe all session data instantly - fresh start.""" |
| profile = QWebEngineProfile.defaultProfile() |
| profile.cookieStore().deleteAllCookies() |
| profile.clearHttpCache() |
| profile.clearAllVisitedLinks() |
| |
| while self._tabs.count() > 0: |
| w = self._tabs.widget(0) |
| self._tabs.removeTab(0) |
| w.deleteLater() |
| self._new_tab() |
| self.statusBar().showMessage("New Identity - all data wiped, fresh session") |
|
|
| |
| def _auto_fill(self): |
| """Fill common form fields automatically.""" |
| fill_data = DATA_DIR / "autofill.json" |
| if not fill_data.exists(): |
| fill_data.write_text(json.dumps({"name": "", "email": "", "phone": "", "address": ""}, indent=2)) |
| self.statusBar().showMessage(f"Edit your info: {fill_data}") |
| |
| if sys.platform == 'win32': |
| os.startfile(str(fill_data)) |
| return |
| |
| data = json.loads(fill_data.read_text()) |
| js = "".join([ |
| f"document.querySelectorAll('input[name*=\"{k}\"],input[type*=\"{k}\"],input[autocomplete*=\"{k}\"],input[placeholder*=\"{k}\"]').forEach(function(el){{el.value='{v}';el.dispatchEvent(new Event('input',{{bubbles:true}}));}});" |
| for k, v in data.items() if v |
| ]) |
| self._run_js(js) |
| self.statusBar().showMessage("Form auto-filled") |
|
|
| |
| def _block_fingerprint(self): |
| """Inject anti-fingerprinting script into page.""" |
| self._run_js(""" |
| (function(){ |
| // Block canvas fingerprinting |
| var origToDataURL = HTMLCanvasElement.prototype.toDataURL; |
| HTMLCanvasElement.prototype.toDataURL = function(){ |
| var ctx = this.getContext('2d'); |
| if(ctx){var imgData=ctx.getImageData(0,0,this.width,this.height); |
| for(var i=0;i<imgData.data.length;i+=4){imgData.data[i]^=1;} |
| ctx.putImageData(imgData,0,0);} |
| return origToDataURL.apply(this,arguments); |
| }; |
| // Spoof navigator properties |
| Object.defineProperty(navigator,'hardwareConcurrency',{get:function(){return 4}}); |
| Object.defineProperty(navigator,'deviceMemory',{get:function(){return 8}}); |
| Object.defineProperty(navigator,'platform',{get:function(){return 'Win32'}}); |
| console.log('MoneyPack: Fingerprint protection active'); |
| })() |
| """) |
| self.statusBar().showMessage("Fingerprint protection injected") |
|
|
| |
| def _turbo_mode(self): |
| """Strip images and heavy content for faster loading.""" |
| self._run_js(""" |
| (function(){ |
| var existing = document.getElementById('mp-turbo'); |
| if(existing){existing.remove();document.querySelectorAll('img,video,iframe').forEach(function(el){el.style.display='';});return} |
| var style = document.createElement('style'); |
| style.id = 'mp-turbo'; |
| style.textContent = 'img,video,iframe,.ad,aside,[class*=sidebar],[class*=banner],[class*=popup]{display:none!important}*{background-image:none!important;animation:none!important;transition:none!important}'; |
| document.head.appendChild(style); |
| })() |
| """) |
| self.statusBar().showMessage("Turbo mode toggled - stripped heavy content") |
|
|
| |
| |
| def _gesture_menu(self): |
| """Quick action gestures menu.""" |
| from PyQt6.QtWidgets import QDialog, QGridLayout |
| |
| dialog = QDialog(self) |
| dialog.setWindowTitle("Quick Actions") |
| dialog.setFixedSize(300, 250) |
| dialog.setStyleSheet("QDialog{background:#0c0c18;border:1px solid #1a1a2a;border-radius:10px;}" |
| "QPushButton{background:#12121e;color:#aaa;border:1px solid #1a1a2a;border-radius:8px;padding:12px;font-size:11px;}" |
| "QPushButton:hover{background:#1a1a30;color:#fff;border-color:#ff1a5c;}") |
| |
| grid = QGridLayout(dialog) |
| grid.setSpacing(8) |
| grid.setContentsMargins(12, 12, 12, 12) |
| |
| actions = [ |
| ("β Back", self._back), ("β Forward", self._fwd), ("β» Reload", self._reload), |
| ("+ New Tab", self._new_tab), ("β Close Tab", self._close_current_tab), ("β Home", self._home), |
| ("β Top", lambda: self._run_js("window.scrollTo(0,0)")), |
| ("β Bottom", lambda: self._run_js("window.scrollTo(0,document.body.scrollHeight)")), |
| ("β Dark Mode", self._force_dark), |
| ] |
| |
| for i, (label, fn) in enumerate(actions): |
| btn = QPushButton(label) |
| btn.clicked.connect(lambda _, f=fn: (f(), dialog.close())) |
| grid.addWidget(btn, i // 3, i % 3) |
| |
| dialog.exec() |
|
|
| |
| def _web_app_mode(self): |
| """Open current page as a standalone minimal window.""" |
| w = self._current_web() |
| if not w: |
| return |
| url = w.url() |
| title = w.title() or "Web App" |
| |
| |
| app_win = QMainWindow(self) |
| app_win.setWindowTitle(title) |
| app_win.resize(1000, 700) |
| app_win.setStyleSheet("QMainWindow{background:#08080e;}") |
| |
| web = QWebEngineView(app_win) |
| page = DarkWebPage(self._profile, web) |
| web.setPage(page) |
| web.setUrl(url) |
| app_win.setCentralWidget(web) |
| app_win.show() |
| |
| self.statusBar().showMessage(f"Opened as Web App: {title}") |
|
|
| |
| def _switch_workspace(self): |
| """Switch between tab workspaces.""" |
| from PyQt6.QtWidgets import QDialog, QListWidget, QListWidgetItem |
| |
| |
| workspaces_file = DATA_DIR / "workspaces.json" |
| workspaces = {} |
| if workspaces_file.exists(): |
| try: |
| workspaces = json.loads(workspaces_file.read_text()) |
| except: |
| pass |
| |
| dialog = QDialog(self) |
| dialog.setWindowTitle("Workspaces") |
| dialog.setFixedSize(350, 300) |
| dialog.setStyleSheet("QDialog{background:#0c0c18;}QListWidget{background:#08080e;color:#aaa;border:1px solid #1a1a2a;border-radius:8px;}QListWidget::item{padding:10px;}QListWidget::item:selected{background:#1a1a30;color:#fff;}") |
| |
| layout = QVBoxLayout(dialog) |
| lbl = QLabel("Workspaces (save & switch tab groups)") |
| lbl.setStyleSheet("color:#d4af37;font-weight:bold;font-size:12px;") |
| layout.addWidget(lbl) |
| |
| lst = QListWidget() |
| for name in workspaces.keys(): |
| count = len(workspaces[name]) |
| lst.addItem(f"{name} ({count} tabs)") |
| layout.addWidget(lst) |
| |
| btn_layout = QHBoxLayout() |
| save_btn = QPushButton("Save Current") |
| save_btn.setStyleSheet("background:#ff1a5c;color:white;border:none;border-radius:6px;padding:8px 16px;") |
| |
| def save_workspace(): |
| name, ok = QInputDialog.getText(dialog, "Save", "Workspace name:") |
| if ok and name: |
| tabs = [] |
| for i in range(self._tabs.count()): |
| tab = self._tabs.widget(i) |
| if hasattr(tab, 'webview'): |
| url = tab.webview.url().toString() |
| if url and not url.startswith("about:"): |
| tabs.append(url) |
| workspaces[name] = tabs |
| workspaces_file.write_text(json.dumps(workspaces, indent=2)) |
| dialog.close() |
| self.statusBar().showMessage(f"Workspace '{name}' saved ({len(tabs)} tabs)") |
| |
| save_btn.clicked.connect(save_workspace) |
| btn_layout.addWidget(save_btn) |
| |
| load_btn = QPushButton("Load Selected") |
| load_btn.setStyleSheet("background:#1a1a2a;color:#aaa;border:1px solid #1a1a2a;border-radius:6px;padding:8px 16px;") |
| |
| def load_workspace(): |
| item = lst.currentItem() |
| if item: |
| name = item.text().split(" (")[0] |
| if name in workspaces: |
| |
| while self._tabs.count() > 0: |
| w = self._tabs.widget(0) |
| self._tabs.removeTab(0) |
| w.deleteLater() |
| |
| for url in workspaces[name]: |
| self._new_tab(url) |
| dialog.close() |
| self.statusBar().showMessage(f"Loaded workspace: {name}") |
| |
| load_btn.clicked.connect(load_workspace) |
| btn_layout.addWidget(load_btn) |
| layout.addLayout(btn_layout) |
| dialog.exec() |
|
|
| |
| def _save_session(self): |
| """Save current tabs as a named session.""" |
| name = f"session_{datetime.now().strftime('%Y%m%d_%H%M')}" |
| tabs = [] |
| for i in range(self._tabs.count()): |
| tab = self._tabs.widget(i) |
| if hasattr(tab, 'webview'): |
| url = tab.webview.url().toString() |
| title = tab.webview.title() or "New Tab" |
| if url and not url.startswith("about:"): |
| tabs.append({"url": url, "title": title}) |
| |
| sessions_file = DATA_DIR / "sessions.json" |
| sessions = {} |
| if sessions_file.exists(): |
| try: |
| sessions = json.loads(sessions_file.read_text()) |
| except: |
| pass |
| sessions[name] = tabs |
| sessions_file.write_text(json.dumps(sessions, indent=2)) |
| self.statusBar().showMessage(f"Session saved: {name} ({len(tabs)} tabs)") |
|
|
| |
| def _suspend_tab(self): |
| """Suspend current tab to save RAM.""" |
| tab = self._tabs.currentWidget() |
| if tab and hasattr(tab, 'webview'): |
| url = tab.webview.url().toString() |
| title = tab.webview.title() or "Suspended" |
| |
| tab.webview.setHtml(f"""<!DOCTYPE html><html><head><style> |
| body{{background:#08080e;color:#555;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;text-align:center;flex-direction:column}} |
| h2{{color:#d4af37;margin-bottom:8px}}a{{color:#ff1a5c;text-decoration:none;padding:10px 20px;border:1px solid #ff1a5c;border-radius:8px;margin-top:16px;display:inline-block}}a:hover{{background:#ff1a5c;color:white}} |
| </style></head><body><h2>Tab Suspended</h2><p>{title}</p><a href="{url}">Click to reload</a><p style="margin-top:20px;color:#333;font-size:10px">Saving RAM - MoneyPack Browser</p></body></html>""", QUrl("about:suspended")) |
| idx = self._tabs.currentIndex() |
| self._tabs.setTabText(idx, f"π€ {title[:15]}") |
| self.statusBar().showMessage(f"Tab suspended: {title}") |
| |
| |
| |
|
|
| |
| def _toggle_compact(self): |
| """Hide/show toolbar for more screen space.""" |
| toolbar = self.findChild(QWidget, "toolbar_widget") |
| if toolbar: |
| toolbar.setVisible(not toolbar.isVisible()) |
| self.statusBar().showMessage("Compact mode" if not toolbar.isVisible() else "Full mode") |
|
|
| |
| def _fire_button(self): |
| """PANIC - destroy ALL browsing data instantly.""" |
| reply = QMessageBox.warning(self, "FIRE", |
| "DESTROY all cookies, cache, history, tabs?", |
| QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) |
| if reply == QMessageBox.StandardButton.Yes: |
| profile = QWebEngineProfile.defaultProfile() |
| profile.cookieStore().deleteAllCookies() |
| profile.clearHttpCache() |
| profile.clearAllVisitedLinks() |
| for f in DATA_DIR.glob("*.json"): |
| try: f.unlink() |
| except: pass |
| while self._tabs.count() > 0: |
| self._tabs.removeTab(0) |
| self._new_tab() |
| self.statusBar().showMessage("ALL DATA DESTROYED") |
|
|
| |
| def _video_assistant(self): |
| """Floating video control bar.""" |
| self._run_js("""(function(){var e=document.getElementById('mp-vassist');if(e){e.remove();return}var v=document.querySelector('video');if(!v)return;var b=document.createElement('div');b.id='mp-vassist';b.style.cssText='position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:rgba(8,8,14,.95);border:1px solid #ff1a5c;border-radius:25px;padding:6px 16px;z-index:999999;display:flex;gap:10px;align-items:center;font-family:system-ui';[['βͺ','v.currentTime-=10'],['β―','v.paused?v.play():v.pause()'],['β©','v.currentTime+=10'],['π','v.muted=!v.muted'],['1x','v.playbackRate=1'],['1.5x','v.playbackRate=1.5'],['2x','v.playbackRate=2'],['PiP','v.requestPictureInPicture()'],['β','b.remove()']].forEach(function(x){var btn=document.createElement('button');btn.textContent=x[0];btn.style.cssText='background:none;border:none;color:#ccc;cursor:pointer;font-size:12px;padding:3px 5px';btn.onmouseover=function(){this.style.color='#ff1a5c'};btn.onmouseout=function(){this.style.color='#ccc'};btn.onclick=function(){eval(x[1])};b.appendChild(btn)});document.body.appendChild(b)})()""") |
|
|
| |
| def _load_userscript(self): |
| """Load JS script from file.""" |
| path, _ = QFileDialog.getOpenFileName(self, "Load Script", "", "JavaScript (*.js)") |
| if path: |
| code = Path(path).read_text() |
| self._run_js(code) |
| self.statusBar().showMessage(f"Script loaded: {os.path.basename(path)}") |
|
|
| |
| def _page_info(self): |
| """Show page statistics.""" |
| self._run_js("""(function(){return JSON.stringify({el:document.getElementsByTagName('*').length,img:document.images.length,js:document.scripts.length,css:document.styleSheets.length,links:document.links.length})})()""", |
| lambda r: QMessageBox.information(self, "Page Info", f"Page Stats:\n{r}") if r else None) |
|
|
| |
| def _view_source(self): |
| """View page source.""" |
| w = self._current_web() |
| if w: |
| w.page().toHtml(lambda html: self._show_source_tab(html)) |
| |
| def _show_source_tab(self, html): |
| if not html: return |
| escaped = html.replace('&','&').replace('<','<').replace('>','>') |
| src_html = f"<!DOCTYPE html><html><head><style>body{{background:#08080e;color:#8be9fd;font-family:Consolas,monospace;font-size:11px;padding:16px;white-space:pre-wrap;margin:0;line-height:1.5}}</style></head><body>{escaped}</body></html>" |
| self._new_tab() |
| tab = self._tabs.currentWidget() |
| if tab: |
| tab.webview.setHtml(src_html, QUrl("about:source")) |
| self._tabs.setTabText(self._tabs.currentIndex(), "Source") |
|
|
| |
| def _file_browser(self): |
| """Built-in file browser.""" |
| path = QFileDialog.getExistingDirectory(self, "Browse", str(Path.home())) |
| if not path: return |
| entries = [] |
| try: |
| for e in sorted(os.scandir(path), key=lambda x: (not x.is_dir(), x.name.lower())): |
| icon = "\U0001f4c1" if e.is_dir() else "\U0001f4c4" |
| size = "" |
| if e.is_file(): |
| try: |
| s = e.stat().st_size |
| for u in ['B','KB','MB','GB']: |
| if s<1024: size=f"{s:.0f}{u}"; break |
| s/=1024 |
| except: pass |
| entries.append(f"<div style='padding:6px;border-bottom:1px solid #1a1a2a'>{icon} {e.name} <span style='color:#444;float:right'>{size}</span></div>") |
| except: entries.append("<div style='color:#ff1a5c'>Permission denied</div>") |
| html = f"<!DOCTYPE html><html><head><style>body{{background:#08080e;color:#aaa;font-family:system-ui;padding:16px;margin:0}}h3{{color:#d4af37}}</style></head><body><h3>{path}</h3>{''.join(entries)}</body></html>" |
| self._new_tab() |
| tab = self._tabs.currentWidget() |
| if tab: |
| tab.webview.setHtml(html, QUrl("about:files")) |
| self._tabs.setTabText(self._tabs.currentIndex(), "Files") |
|
|
| |
| def _link_hints(self): |
| """Number every link - type number to click without mouse.""" |
| self._run_js("""(function(){var e=document.getElementById('mp-hints');if(e){e.remove();return}var links=document.querySelectorAll('a[href],button');var c=document.createElement('div');c.id='mp-hints';c.style.cssText='position:absolute;top:0;left:0;width:100%;height:100%;z-index:999999;pointer-events:none';var hs=[];links.forEach(function(l,i){if(i>50)return;var r=l.getBoundingClientRect();if(r.top<0||r.top>innerHeight)return;var h=document.createElement('span');h.textContent=i+1;h.style.cssText='position:fixed;top:'+r.top+'px;left:'+r.left+'px;background:#ff1a5c;color:#fff;font-size:9px;padding:1px 3px;border-radius:2px;z-index:999999;font-family:monospace';c.appendChild(h);hs.push(l)});document.body.appendChild(c);var inp='';function k(e){if(e.key==='Escape'){c.remove();document.removeEventListener('keydown',k);return}if(e.key>='0'&&e.key<='9'){inp+=e.key;if(inp.length>=2||parseInt(inp)*10>hs.length){var idx=parseInt(inp)-1;if(idx>=0&&idx<hs.length)hs[idx].click();c.remove();document.removeEventListener('keydown',k)}}}document.addEventListener('keydown',k);setTimeout(function(){c.remove();document.removeEventListener('keydown',k)},5000)})()""") |
| self.statusBar().showMessage("Link hints - type number, Esc to cancel") |
|
|
| |
| def _history_view(self): |
| """View browsing history.""" |
| history_file = DATA_DIR / "history.json" |
| entries = [] |
| if history_file.exists(): |
| try: entries = json.loads(history_file.read_text())[:50] |
| except: pass |
| rows = "" |
| for h in entries: |
| t = h.get('title', '?')[:45] |
| u = h.get('url', '') |
| tm = h.get('time', '')[:16] |
| rows += f"<div style='padding:8px;border-bottom:1px solid #1a1a2a'><a href='{u}' style='color:#00e5ff;text-decoration:none'>{t}</a><span style='color:#333;float:right;font-size:10px'>{tm}</span></div>" |
| html = f"<!DOCTYPE html><html><head><style>body{{background:#08080e;color:#aaa;font-family:system-ui;padding:16px;margin:0}}h3{{color:#d4af37}}a:hover{{color:#ff1a5c}}</style></head><body><h3>History</h3>{rows or '<p>No history</p>'}</body></html>" |
| self._new_tab() |
| tab = self._tabs.currentWidget() |
| if tab: |
| tab.webview.setHtml(html, QUrl("about:history")) |
| self._tabs.setTabText(self._tabs.currentIndex(), "History") |
|
|
| |
| |
| |
|
|
| |
| def _url_notes(self): |
| """Save notes linked to the current URL.""" |
| w = self._current_web() |
| if not w: return |
| url = w.url().host() or "general" |
| notes_dir = DATA_DIR / "url_notes" |
| notes_dir.mkdir(exist_ok=True) |
| note_file = notes_dir / f"{url.replace('.','_')}.txt" |
| |
| from PyQt6.QtWidgets import QDialog, QPlainTextEdit |
| dialog = QDialog(self) |
| dialog.setWindowTitle(f"Notes for: {url}") |
| dialog.setFixedSize(400, 250) |
| dialog.setStyleSheet("QDialog{background:#0c0c18;}QPlainTextEdit{background:#08080e;color:#ccc;border:1px solid #1a1a2a;border-radius:8px;padding:8px;font-size:12px;}") |
| layout = QVBoxLayout(dialog) |
| te = QPlainTextEdit() |
| if note_file.exists(): |
| te.setPlainText(note_file.read_text()) |
| te.setPlaceholderText(f"Notes about {url}...") |
| layout.addWidget(te) |
| dialog.finished.connect(lambda: note_file.write_text(te.toPlainText())) |
| dialog.exec() |
|
|
| |
| def _force_https(self): |
| """Upgrade current page to HTTPS.""" |
| w = self._current_web() |
| if w: |
| url = w.url().toString() |
| if url.startswith("http://"): |
| new_url = url.replace("http://", "https://", 1) |
| w.setUrl(QUrl(new_url)) |
| self.statusBar().showMessage(f"Upgraded to HTTPS") |
| else: |
| self.statusBar().showMessage("Already HTTPS") |
|
|
| |
| def _connection_monitor(self): |
| """Show what domains the page connects to.""" |
| self._run_js("""(function(){ |
| var entries = performance.getEntriesByType('resource'); |
| var domains = {}; |
| entries.forEach(function(e){ |
| try{var h=new URL(e.name).hostname;domains[h]=(domains[h]||0)+1}catch(ex){} |
| }); |
| var sorted = Object.entries(domains).sort(function(a,b){return b[1]-a[1]}); |
| var text = sorted.map(function(d){return d[0]+' ('+d[1]+')'}).join('\\n'); |
| return text || 'No connections detected'; |
| })()""", lambda result: QMessageBox.information(self, "Connections", f"Domains this page connected to:\\n\\n{result}") if result else None) |
|
|
| |
| def _ssl_check(self): |
| """Check if current page is secure.""" |
| w = self._current_web() |
| if not w: return |
| url = w.url().toString() |
| if url.startswith("https://"): |
| self.statusBar().showMessage(f"SECURE: {w.url().host()} (HTTPS)") |
| QMessageBox.information(self, "SSL Status", f"Connection is SECURE (HTTPS)\n\nHost: {w.url().host()}\nProtocol: TLS encrypted") |
| else: |
| self.statusBar().showMessage(f"NOT SECURE: {w.url().host()}") |
| QMessageBox.warning(self, "SSL Status", f"Connection is NOT SECURE\n\nHost: {w.url().host()}\nNo encryption - data sent in plain text!") |
|
|
| |
| def _annotate_page(self): |
| """Leave a visible note ON the current page.""" |
| text, ok = QInputDialog.getText(self, "Annotate", "Add note to this page:") |
| if ok and text: |
| self._run_js(f"""(function(){{ |
| var note=document.createElement('div'); |
| note.style.cssText='position:fixed;top:10px;right:10px;background:rgba(255,26,92,0.95);color:white;padding:10px 16px;border-radius:8px;z-index:999999;font-family:system-ui;font-size:12px;max-width:250px;cursor:pointer;box-shadow:0 4px 15px rgba(0,0,0,0.5)'; |
| note.textContent='{text}'; |
| note.onclick=function(){{this.remove()}}; |
| document.body.appendChild(note); |
| }})()""") |
|
|
| |
| def _quick_launch(self): |
| """Show quick launch bar with top sites.""" |
| from PyQt6.QtWidgets import QDialog |
| dialog = QDialog(self) |
| dialog.setWindowTitle("Quick Launch") |
| dialog.setFixedSize(500, 80) |
| dialog.setStyleSheet("QDialog{background:#0c0c18;border:1px solid #1a1a2a;border-radius:10px;}") |
| layout = QHBoxLayout(dialog) |
| layout.setContentsMargins(10, 10, 10, 10) |
| sites = [("Google","https://google.com"),("YouTube","https://youtube.com"),("GitHub","https://github.com"),("Reddit","https://reddit.com"),("Twitter","https://twitter.com"),("Discord","https://discord.com"),("ChatGPT","https://chat.openai.com"),("Netflix","https://netflix.com")] |
| for name, url in sites: |
| btn = QPushButton(name) |
| btn.setStyleSheet("QPushButton{background:#12121e;color:#aaa;border:1px solid #1a1a2a;border-radius:6px;padding:8px 4px;font-size:9px;font-weight:bold;}QPushButton:hover{background:#1a1a30;color:#fff;border-color:#ff1a5c;}") |
| btn.clicked.connect(lambda _, u=url: (self._new_tab(u), dialog.close())) |
| layout.addWidget(btn) |
| dialog.exec() |
|
|
| |
| def _custom_block_rules(self): |
| """Edit custom URL block rules.""" |
| rules_file = DATA_DIR / "block_rules.txt" |
| if not rules_file.exists(): |
| rules_file.write_text("# Add domains to block (one per line)\n# Example:\n# ads.example.com\n# tracker.bad.com\n") |
| |
| from PyQt6.QtWidgets import QDialog, QPlainTextEdit |
| dialog = QDialog(self) |
| dialog.setWindowTitle("Custom Block Rules") |
| dialog.setFixedSize(400, 300) |
| dialog.setStyleSheet("QDialog{background:#0c0c18;}QPlainTextEdit{background:#08080e;color:#ccc;border:1px solid #1a1a2a;border-radius:8px;padding:8px;font-family:Consolas;font-size:11px;}") |
| layout = QVBoxLayout(dialog) |
| lbl = QLabel("Add domains to block (one per line):") |
| lbl.setStyleSheet("color:#aaa;") |
| layout.addWidget(lbl) |
| te = QPlainTextEdit() |
| te.setPlainText(rules_file.read_text()) |
| layout.addWidget(te) |
| |
| def save_rules(): |
| rules_file.write_text(te.toPlainText()) |
| |
| for line in te.toPlainText().splitlines(): |
| line = line.strip() |
| if line and not line.startswith('#'): |
| AD_HOSTS.add(line) |
| self.statusBar().showMessage(f"Block rules updated ({len(AD_HOSTS)} domains)") |
| |
| dialog.finished.connect(lambda: save_rules()) |
| dialog.exec() |
|
|
| |
| def _inject_css(self): |
| """Apply custom CSS to current page.""" |
| css, ok = QInputDialog.getText(self, "Inject CSS", "CSS code (e.g. body{background:black}):") |
| if ok and css: |
| escaped = css.replace("'", "\\'").replace("\n", " ") |
| self._run_js(f"(function(){{var s=document.createElement('style');s.textContent='{escaped}';document.head.appendChild(s)}})()") |
| self.statusBar().showMessage("Custom CSS injected") |
|
|
| |
| def _set_tab_expire(self): |
| """Set current tab to auto-close after X minutes.""" |
| minutes, ok = QInputDialog.getInt(self, "Auto-Close Timer", "Close this tab in (minutes):", 30, 1, 1440) |
| if ok: |
| tab = self._tabs.currentWidget() |
| idx = self._tabs.currentIndex() |
| title = self._tabs.tabText(idx) |
| self._tabs.setTabText(idx, f"[{minutes}m] {title}") |
| QTimer.singleShot(minutes * 60 * 1000, lambda: self._expire_tab(tab)) |
| self.statusBar().showMessage(f"Tab will auto-close in {minutes} minutes") |
| |
| def _expire_tab(self, tab): |
| idx = self._tabs.indexOf(tab) |
| if idx >= 0: |
| self._close_tab(idx) |
|
|
| |
| def _isolated_tab(self): |
| """Open a tab with its own isolated session (separate cookies/login).""" |
| |
| profile = QWebEngineProfile(f"isolated_{id(self)}_{self._tabs.count()}", self) |
| profile.setHttpUserAgent(USER_AGENT) |
| profile.setPersistentCookiesPolicy(QWebEngineProfile.PersistentCookiesPolicy.NoPersistentCookies) |
| |
| tab = BrowserTab(profile, open_tab_callback=self._open_popup_tab, parent=self) |
| tab.set_status_callback(lambda msg: self.statusBar().showMessage(msg)) |
| tab.go_home() |
| |
| tab.webview.titleChanged.connect(lambda title, t=tab: self._update_tab_title(t, title)) |
| tab.webview.urlChanged.connect(lambda qurl, t=tab: self._update_url_bar(t, qurl)) |
| tab.webview.iconChanged.connect(lambda icon, t=tab: self._update_tab_icon(t, icon)) |
| |
| idx = self._tabs.addTab(tab, "\U0001f510 Isolated") |
| self._tabs.setCurrentIndex(idx) |
| self.statusBar().showMessage("Isolated tab - separate session, no shared cookies") |
|
|
| |
| |
| |
|
|
| |
| def _responsive_preview(self): |
| """View page at different screen sizes.""" |
| w = self._current_web() |
| if not w: return |
| url = w.url().toString() |
| sizes = [("Mobile", 375, 667), ("Tablet", 768, 1024), ("Desktop", 1440, 900)] |
| |
| from PyQt6.QtWidgets import QDialog, QSplitter |
| dialog = QDialog(self) |
| dialog.setWindowTitle("Responsive Preview") |
| dialog.resize(1200, 700) |
| dialog.setStyleSheet("QDialog{background:#08080e;}") |
| layout = QVBoxLayout(dialog) |
| splitter = QSplitter(Qt.Orientation.Horizontal) |
| |
| for name, width, height in sizes: |
| frame = QWidget() |
| fl = QVBoxLayout(frame) |
| fl.setContentsMargins(4, 4, 4, 4) |
| lbl = QLabel(f"{name} ({width}x{height})") |
| lbl.setStyleSheet("color:#888;font-size:10px;") |
| lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) |
| fl.addWidget(lbl) |
| view = QWebEngineView() |
| page = DarkWebPage(self._profile, view) |
| view.setPage(page) |
| view.setFixedWidth(min(width, 380)) |
| view.setUrl(QUrl(url)) |
| fl.addWidget(view) |
| splitter.addWidget(frame) |
| |
| layout.addWidget(splitter) |
| dialog.exec() |
|
|
| |
| def _emulate_device(self): |
| """Switch user-agent to emulate a mobile device.""" |
| devices = [ |
| ("Desktop (default)", USER_AGENT), |
| ("iPhone 15", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"), |
| ("Android Phone", "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36"), |
| ("iPad", "Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"), |
| ("Bot/Crawler", "MoneyPackBot/1.0"), |
| ] |
| items = [d[0] for d in devices] |
| choice, ok = QInputDialog.getItem(self, "Device Emulation", "Select device:", items, 0, False) |
| if ok: |
| for name, ua in devices: |
| if name == choice: |
| self._profile.setHttpUserAgent(ua) |
| self._reload() |
| self.statusBar().showMessage(f"Emulating: {name}") |
| break |
|
|
| |
| def _page_timer(self): |
| """Show how long the current page took to load.""" |
| self._run_js("""(function(){ |
| var timing = performance.timing || performance.getEntriesByType('navigation')[0]; |
| if(timing.loadEventEnd && timing.navigationStart){ |
| return (timing.loadEventEnd - timing.navigationStart) + 'ms'; |
| } |
| var nav = performance.getEntriesByType('navigation'); |
| if(nav.length > 0) return Math.round(nav[0].loadEventEnd) + 'ms'; |
| return 'Timing not available'; |
| })()""", lambda r: self.statusBar().showMessage(f"Page load time: {r}") if r else None) |
|
|
| |
| def _tab_overview(self): |
| """Show all tabs as a visual grid.""" |
| from PyQt6.QtWidgets import QDialog, QGridLayout |
| |
| dialog = QDialog(self) |
| dialog.setWindowTitle("Tab Overview") |
| dialog.resize(700, 500) |
| dialog.setStyleSheet("QDialog{background:#08080e;}QPushButton{background:#12121e;color:#aaa;border:1px solid #1a1a2a;border-radius:8px;padding:16px 8px;font-size:10px;text-align:center;}QPushButton:hover{border-color:#ff1a5c;color:#fff;}") |
| |
| layout = QGridLayout(dialog) |
| layout.setSpacing(8) |
| |
| for i in range(self._tabs.count()): |
| tab = self._tabs.widget(i) |
| title = self._tabs.tabText(i) |
| btn = QPushButton(f"{title}\n\n{'[active]' if i == self._tabs.currentIndex() else ''}") |
| btn.setFixedHeight(100) |
| btn.clicked.connect(lambda _, idx=i: (self._tabs.setCurrentIndex(idx), dialog.close())) |
| layout.addWidget(btn, i // 4, i % 4) |
| |
| dialog.exec() |
|
|
| |
| def _clipboard_history(self): |
| """Show clipboard and allow paste from history.""" |
| clip = QApplication.clipboard() |
| current = clip.text() |
| |
| |
| clip_file = DATA_DIR / "clipboard.json" |
| history = [] |
| if clip_file.exists(): |
| try: history = json.loads(clip_file.read_text()) |
| except: pass |
| |
| |
| if current and (not history or history[0] != current): |
| history.insert(0, current) |
| history = history[:20] |
| clip_file.write_text(json.dumps(history)) |
| |
| from PyQt6.QtWidgets import QDialog, QListWidget |
| dialog = QDialog(self) |
| dialog.setWindowTitle("Clipboard History") |
| dialog.setFixedSize(400, 300) |
| dialog.setStyleSheet("QDialog{background:#0c0c18;}QListWidget{background:#08080e;color:#ccc;border:1px solid #1a1a2a;border-radius:8px;}QListWidget::item{padding:8px;border-bottom:1px solid #1a1a2a;}QListWidget::item:selected{background:#1a1a30;}") |
| layout = QVBoxLayout(dialog) |
| lst = QListWidget() |
| for item in history: |
| lst.addItem(item[:60] + ("..." if len(item) > 60 else "")) |
| layout.addWidget(lst) |
| |
| def paste_item(item): |
| idx = lst.row(item) |
| if 0 <= idx < len(history): |
| clip.setText(history[idx]) |
| self._run_js(f"document.execCommand('insertText', false, '{history[idx][:200]}')") |
| dialog.close() |
| |
| lst.itemDoubleClicked.connect(paste_item) |
| dialog.exec() |
|
|
| |
| def _read_aloud(self): |
| """Read the page content aloud using browser TTS.""" |
| self._run_js("""(function(){ |
| if(window.speechSynthesis.speaking){window.speechSynthesis.cancel();return} |
| var text = document.body.innerText.substring(0, 3000); |
| var utterance = new SpeechSynthesisUtterance(text); |
| utterance.rate = 1.0; |
| utterance.pitch = 1.0; |
| window.speechSynthesis.speak(utterance); |
| })()""") |
| self.statusBar().showMessage("Reading page aloud (click again to stop)") |
|
|
| |
| def _share_qr(self): |
| """Generate QR code of current URL to share.""" |
| w = self._current_web() |
| if not w: return |
| url = w.url().toString() |
| |
| qr_html = f"""<!DOCTYPE html><html><head><style> |
| body{{background:#08080e;color:#aaa;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;flex-direction:column}} |
| img{{border-radius:12px;margin:16px}}p{{font-size:11px;color:#555;max-width:300px;word-break:break-all;text-align:center}} |
| h3{{color:#d4af37}} |
| </style></head><body> |
| <h3>Share this page</h3> |
| <img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={urllib.parse.quote(url)}" alt="QR Code"> |
| <p>{url}</p> |
| </body></html>""" |
| |
| self._new_tab() |
| tab = self._tabs.currentWidget() |
| if tab: |
| tab.webview.setHtml(qr_html, QUrl("about:share")) |
| self._tabs.setTabText(self._tabs.currentIndex(), "Share QR") |
|
|
| |
| def _productivity_stats(self): |
| """Show browsing stats for this session.""" |
| tabs_open = self._tabs.count() |
| ads_blocked = self._ad_blocker.blocked_count |
| |
| |
| bm_count = len(BookmarkManager.load()) |
| |
| html = f"""<!DOCTYPE html><html><head><style> |
| body{{background:#08080e;color:#aaa;font-family:system-ui;padding:30px;margin:0}} |
| h2{{color:#d4af37;margin-bottom:20px}} |
| .stat{{display:flex;justify-content:space-between;padding:12px 0;border-bottom:1px solid #1a1a2a}} |
| .stat .val{{color:#ff1a5c;font-weight:bold;font-size:18px}} |
| .stat .label{{color:#666}} |
| </style></head><body> |
| <h2>Session Stats</h2> |
| <div class="stat"><span class="label">Tabs Open</span><span class="val">{tabs_open}</span></div> |
| <div class="stat"><span class="label">Ads Blocked</span><span class="val">{ads_blocked}</span></div> |
| <div class="stat"><span class="label">Bookmarks</span><span class="val">{bm_count}</span></div> |
| <div class="stat"><span class="label">Data Dir</span><span class="val" style="font-size:10px">{DATA_DIR}</span></div> |
| <div class="stat"><span class="label">Version</span><span class="val">{APP_VER}</span></div> |
| <div class="stat"><span class="label">Engine</span><span class="val">Chromium (Qt WebEngine)</span></div> |
| </body></html>""" |
| |
| self._new_tab() |
| tab = self._tabs.currentWidget() |
| if tab: |
| tab.webview.setHtml(html, QUrl("about:stats")) |
| self._tabs.setTabText(self._tabs.currentIndex(), "Stats") |
|
|
| |
| def _scroll_top(self): |
| self._run_js("window.scrollTo({top:0,behavior:'smooth'})") |
| |
| def _scroll_bottom(self): |
| self._run_js("window.scrollTo({top:document.body.scrollHeight,behavior:'smooth'})") |
|
|
| |
| |
| |
|
|
| |
| def _pin_tab(self): |
| """Pin current tab (moves to front, short title).""" |
| idx = self._tabs.currentIndex() |
| tab = self._tabs.widget(idx) |
| if not tab: return |
| title = self._tabs.tabText(idx) |
| if title.startswith("\U0001f4cc"): |
| |
| self._tabs.setTabText(idx, title[2:]) |
| self.statusBar().showMessage("Tab unpinned") |
| else: |
| |
| self._tabs.tabBar().moveTab(idx, 0) |
| self._tabs.setTabText(0, f"\U0001f4cc{title[:8]}") |
| self.statusBar().showMessage("Tab pinned") |
|
|
| |
| def _mute_tab(self): |
| """Mute/unmute audio on current tab.""" |
| tab = self._tabs.currentWidget() |
| if tab and hasattr(tab, 'webview'): |
| page = tab.webview.page() |
| muted = page.isAudioMuted() |
| page.setAudioMuted(not muted) |
| idx = self._tabs.currentIndex() |
| title = self._tabs.tabText(idx) |
| if not muted: |
| self._tabs.setTabText(idx, f"\U0001f507 {title}") |
| self.statusBar().showMessage("Tab muted") |
| else: |
| self._tabs.setTabText(idx, title.replace("\U0001f507 ", "")) |
| self.statusBar().showMessage("Tab unmuted") |
|
|
| |
| def _page_performance(self): |
| """Show page performance metrics.""" |
| self._run_js("""(function(){ |
| var nav = performance.getEntriesByType('navigation')[0] || {}; |
| var paint = performance.getEntriesByType('paint'); |
| var fcp = paint.find(function(p){return p.name==='first-contentful-paint'}); |
| var resources = performance.getEntriesByType('resource'); |
| var totalSize = resources.reduce(function(s,r){return s+(r.transferSize||0)},0); |
| return JSON.stringify({ |
| dns: Math.round(nav.domainLookupEnd - nav.domainLookupStart) || 0, |
| connect: Math.round(nav.connectEnd - nav.connectStart) || 0, |
| ttfb: Math.round(nav.responseStart - nav.requestStart) || 0, |
| load: Math.round(nav.loadEventEnd - nav.startTime) || 0, |
| fcp: fcp ? Math.round(fcp.startTime) : 0, |
| resources: resources.length, |
| transferred: totalSize |
| }); |
| })()""", self._show_performance) |
| |
| def _show_performance(self, result): |
| if not result: return |
| try: |
| d = json.loads(result) |
| score = 100 |
| if d['load'] > 3000: score -= 30 |
| elif d['load'] > 1500: score -= 15 |
| if d['fcp'] > 2000: score -= 20 |
| elif d['fcp'] > 1000: score -= 10 |
| if d['resources'] > 100: score -= 10 |
| score = max(0, score) |
| |
| grade = "A" if score >= 90 else "B" if score >= 70 else "C" if score >= 50 else "D" if score >= 30 else "F" |
| msg = (f"Performance Score: {score}/100 (Grade: {grade})\n\n" |
| f"DNS Lookup: {d['dns']}ms\n" |
| f"Connection: {d['connect']}ms\n" |
| f"Time to First Byte: {d['ttfb']}ms\n" |
| f"First Paint: {d['fcp']}ms\n" |
| f"Full Load: {d['load']}ms\n" |
| f"Resources: {d['resources']}\n" |
| f"Transferred: {d['transferred']//1024}KB") |
| QMessageBox.information(self, "Page Performance", msg) |
| except: pass |
|
|
| |
| def _save_for_later(self): |
| """Save current page to reading list.""" |
| w = self._current_web() |
| if not w: return |
| reading_file = DATA_DIR / "reading_list.json" |
| reading = [] |
| if reading_file.exists(): |
| try: reading = json.loads(reading_file.read_text()) |
| except: pass |
| reading.insert(0, {"title": w.title(), "url": w.url().toString(), "saved": datetime.now().isoformat()}) |
| reading_file.write_text(json.dumps(reading[:100], indent=2)) |
| self.statusBar().showMessage(f"Saved for later: {w.title()}") |
|
|
| |
| def _reading_list(self): |
| """View saved articles.""" |
| reading_file = DATA_DIR / "reading_list.json" |
| items = [] |
| if reading_file.exists(): |
| try: items = json.loads(reading_file.read_text()) |
| except: pass |
| rows = "" |
| for item in items[:30]: |
| rows += f"<div style='padding:10px;border-bottom:1px solid #1a1a2a'><a href='{item['url']}' style='color:#00e5ff;text-decoration:none;font-size:13px'>{item['title'][:50]}</a><br><span style='color:#333;font-size:9px'>{item.get('saved','')[:10]}</span></div>" |
| html = f"<!DOCTYPE html><html><head><style>body{{background:#08080e;color:#aaa;font-family:system-ui;padding:20px;margin:0}}h2{{color:#d4af37}}a:hover{{color:#ff1a5c}}</style></head><body><h2>Reading List</h2>{rows or '<p>Empty</p>'}</body></html>" |
| self._new_tab() |
| tab = self._tabs.currentWidget() |
| if tab: |
| tab.webview.setHtml(html, QUrl("about:reading")) |
| self._tabs.setTabText(self._tabs.currentIndex(), "Reading List") |
|
|
| |
| def _toggle_javascript(self): |
| """Toggle JavaScript on current tab.""" |
| tab = self._tabs.currentWidget() |
| if tab and hasattr(tab, 'webview'): |
| s = tab.webview.page().settings() |
| current = s.testAttribute(QWebEngineSettings.WebAttribute.JavascriptEnabled) |
| s.setAttribute(QWebEngineSettings.WebAttribute.JavascriptEnabled, not current) |
| self._reload() |
| self.statusBar().showMessage(f"JavaScript: {'OFF' if current else 'ON'}") |
|
|
| |
| def _search_tabs(self): |
| """Search open tabs by title.""" |
| text, ok = QInputDialog.getText(self, "Search Tabs", "Find tab:") |
| if ok and text: |
| text_lower = text.lower() |
| for i in range(self._tabs.count()): |
| tab = self._tabs.widget(i) |
| if hasattr(tab, 'webview'): |
| title = tab.webview.title() or "" |
| if text_lower in title.lower(): |
| self._tabs.setCurrentIndex(i) |
| self.statusBar().showMessage(f"Found: {title}") |
| return |
| self.statusBar().showMessage("Tab not found") |
|
|
| |
| def _generate_temp_email(self): |
| """Show a temporary email idea (informational).""" |
| import random, string |
| rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) |
| fake_email = f"moneypack.{rand}@duck.com" |
| QMessageBox.information(self, "Temp Email", |
| f"Use this for signups you don't trust:\n\n{fake_email}\n\n" |
| f"(Note: This is a generated address format.\n" |
| f"For real temp email, visit tempmail.com)") |
| |
| QApplication.clipboard().setText(fake_email) |
| self.statusBar().showMessage(f"Copied to clipboard: {fake_email}") |
|
|
| |
| def _color_picker(self): |
| """Pick any color from the page.""" |
| self._run_js("""(function(){ |
| if(document.getElementById('mp-picker')){document.getElementById('mp-picker').remove();return} |
| var overlay = document.createElement('div'); |
| overlay.id = 'mp-picker'; |
| overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:999999;cursor:crosshair;'; |
| overlay.onclick = function(e){ |
| var el = document.elementFromPoint(e.clientX, e.clientY); |
| overlay.remove(); |
| if(el){ |
| var color = getComputedStyle(el).backgroundColor; |
| var hex = color; |
| try{ |
| var rgb = color.match(/\\d+/g); |
| if(rgb) hex = '#' + rgb.slice(0,3).map(function(x){return parseInt(x).toString(16).padStart(2,'0')}).join(''); |
| }catch(ex){} |
| navigator.clipboard.writeText(hex); |
| var toast = document.createElement('div'); |
| toast.style.cssText = 'position:fixed;top:10px;right:10px;background:'+hex+';color:white;padding:10px 16px;border-radius:8px;z-index:999999;font-family:monospace;font-size:14px;text-shadow:0 0 3px black'; |
| toast.textContent = hex + ' (copied!)'; |
| document.body.appendChild(toast); |
| setTimeout(function(){toast.remove()},3000); |
| } |
| }; |
| document.body.appendChild(overlay); |
| })()""") |
| self.statusBar().showMessage("Click any element to pick its color") |
|
|
| |
| def _narrow_mode(self): |
| """Toggle narrow window for one-hand/mobile-like use.""" |
| if self.width() > 500: |
| self._saved_size = self.size() |
| self.resize(400, self.height()) |
| self.statusBar().showMessage("Narrow mode") |
| else: |
| if hasattr(self, '_saved_size'): |
| self.resize(self._saved_size) |
| else: |
| self.resize(1350, 820) |
| self.statusBar().showMessage("Full width restored") |
|
|
| |
| def _save_zoom(self): |
| """Remember zoom level for this site.""" |
| w = self._current_web() |
| if not w: return |
| host = w.url().host() |
| zoom = w.zoomFactor() |
| |
| zoom_file = DATA_DIR / "zoom_memory.json" |
| zooms = {} |
| if zoom_file.exists(): |
| try: zooms = json.loads(zoom_file.read_text()) |
| except: pass |
| zooms[host] = zoom |
| zoom_file.write_text(json.dumps(zooms)) |
| self.statusBar().showMessage(f"Zoom {int(zoom*100)}% saved for {host}") |
|
|
| |
| def _show_session_info(self): |
| """Show which session/profile the current tab is using.""" |
| tab = self._tabs.currentWidget() |
| if tab and hasattr(tab, 'webview'): |
| page = tab.webview.page() |
| profile = page.profile() |
| name = profile.storageName() or "default" |
| persistent = profile.persistentStoragePath() or "none" |
| cookies = "persistent" if profile.persistentCookiesPolicy() == QWebEngineProfile.PersistentCookiesPolicy.AllowPersistentCookies else "session only" |
| QMessageBox.information(self, "Session Info", |
| f"Profile: {name}\n" |
| f"Storage: {persistent[:50]}\n" |
| f"Cookies: {cookies}\n" |
| f"User-Agent: {profile.httpUserAgent()[:60]}...") |
|
|
| |
| def _about(self): |
| QMessageBox.about( |
| self, "About", |
| f"{APP_NAME} v{APP_VER}\n" |
| f"Created by MoneyPack\n\n" |
| f"Features:\n" |
| f"- Pure dark UI (zero white flash)\n" |
| f"- AI Sidebar (5 free AI chat services)\n" |
| f"- Ad Blocker ({self._ad_blocker.blocked_count} ads blocked)\n" |
| f"- Download Manager (save any file)\n" |
| f"- Fullscreen Video Support (YouTube etc)\n" |
| f"- Popup/New Window Handling\n" |
| f"- Video: Max quality, PiP, Theater, Enhance\n" |
| f"- Private mode (no cookies saved)\n" |
| f"- Persistent sessions (logins stay)\n" |
| f"- Bookmarks, Find, Zoom\n" |
| f"- Chrome user-agent (no site blocking)\n" |
| f"- Tab favicons\n" |
| f"- Link hover preview\n" |
| f"- Keyboard shortcuts\n" |
| f"\nProfile: {DATA_DIR}" |
| ) |
|
|
| def keyPressEvent(self, event): |
| """Escape exits fullscreen.""" |
| if event.key() == Qt.Key.Key_Escape and self.isFullScreen(): |
| self.showNormal() |
| else: |
| super().keyPressEvent(event) |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| app = QApplication(sys.argv) |
| app.setApplicationName(APP_NAME) |
| app.setFont(QFont("Segoe UI", 10)) |
|
|
| |
| palette = app.palette() |
| dark = QColor("#08080e") |
| palette.setColor(QPalette.ColorRole.Window, dark) |
| palette.setColor(QPalette.ColorRole.Base, dark) |
| palette.setColor(QPalette.ColorRole.WindowText, QColor("#aaaaaa")) |
| palette.setColor(QPalette.ColorRole.Text, QColor("#cccccc")) |
| palette.setColor(QPalette.ColorRole.Button, dark) |
| palette.setColor(QPalette.ColorRole.ButtonText, QColor("#aaaaaa")) |
| app.setPalette(palette) |
|
|
| |
| splash_pix = QPixmap(520, 320) |
| splash_pix.fill(QColor("#08080e")) |
| |
| painter = QPainter(splash_pix) |
| painter.setRenderHint(QPainter.RenderHint.Antialiasing) |
| |
| |
| bg_grad = QLinearGradient(0, 0, 520, 320) |
| bg_grad.setColorAt(0, QColor("#08080e")) |
| bg_grad.setColorAt(0.5, QColor("#0c0c1a")) |
| bg_grad.setColorAt(1, QColor("#08080e")) |
| painter.fillRect(0, 0, 520, 320, bg_grad) |
| |
| |
| glow = QRadialGradient(260, 120, 100) |
| glow.setColorAt(0, QColor(255, 26, 92, 30)) |
| glow.setColorAt(1, QColor(255, 26, 92, 0)) |
| painter.setBrush(QBrush(glow)) |
| painter.setPen(Qt.PenStyle.NoPen) |
| painter.drawEllipse(160, 50, 200, 140) |
| |
| |
| painter.setPen(Qt.PenStyle.NoPen) |
| grad = QLinearGradient(140, 100, 380, 100) |
| grad.setColorAt(0, QColor("#ff1a5c")) |
| grad.setColorAt(1, QColor("#d4af37")) |
| painter.setPen(QPen(QBrush(grad), 1)) |
| painter.setFont(QFont("Segoe UI", 36, QFont.Weight.Black)) |
| painter.drawText(QRect(0, 80, 520, 60), Qt.AlignmentFlag.AlignCenter, "MoneyPack") |
| |
| |
| painter.setPen(QColor("#444455")) |
| painter.setFont(QFont("Segoe UI", 10, QFont.Weight.Normal)) |
| painter.drawText(QRect(0, 140, 520, 25), Qt.AlignmentFlag.AlignCenter, "B R O W S E R") |
| |
| |
| painter.setPen(QColor("#333340")) |
| painter.setFont(QFont("Segoe UI", 8)) |
| painter.drawText(QRect(0, 168, 520, 20), Qt.AlignmentFlag.AlignCenter, f"v{APP_VER} | 60 Features | Created by MoneyPack") |
| |
| |
| bar_x, bar_y, bar_w, bar_h = 130, 220, 260, 6 |
| painter.setBrush(QColor("#1a1a2a")) |
| painter.setPen(Qt.PenStyle.NoPen) |
| painter.drawRoundedRect(bar_x, bar_y, bar_w, bar_h, 3, 3) |
| |
| |
| bar_grad = QLinearGradient(bar_x, 0, bar_x + bar_w, 0) |
| bar_grad.setColorAt(0, QColor("#ff1a5c")) |
| bar_grad.setColorAt(1, QColor("#d4af37")) |
| painter.setBrush(QBrush(bar_grad)) |
| painter.drawRoundedRect(bar_x, bar_y, int(bar_w * 0.7), bar_h, 3, 3) |
| |
| |
| painter.setPen(QColor("#555566")) |
| painter.setFont(QFont("Segoe UI", 9)) |
| painter.drawText(QRect(0, 240, 520, 20), Qt.AlignmentFlag.AlignCenter, "Loading browser engine...") |
| |
| |
| line_grad = QLinearGradient(60, 290, 460, 290) |
| line_grad.setColorAt(0, QColor("#ff1a5c00")) |
| line_grad.setColorAt(0.5, QColor("#ff1a5c44")) |
| line_grad.setColorAt(1, QColor("#ff1a5c00")) |
| painter.setPen(QPen(QBrush(line_grad), 1)) |
| painter.drawLine(60, 290, 460, 290) |
| |
| |
| painter.setPen(QColor("#222230")) |
| painter.setFont(QFont("Segoe UI", 7)) |
| painter.drawText(QRect(0, 298, 520, 15), Qt.AlignmentFlag.AlignCenter, "moneypack.dev") |
| |
| painter.end() |
| |
| |
| from PyQt6.QtWidgets import QSplashScreen |
| splash = QSplashScreen(splash_pix) |
| splash.setWindowFlags(Qt.WindowType.SplashScreen | Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint) |
| splash.show() |
| app.processEvents() |
| |
| |
| import time |
| steps = ["Initializing engine...", "Loading ad blocker...", "Setting up profiles...", "Preparing UI...", "Almost ready..."] |
| for i, step in enumerate(steps): |
| splash.showMessage(f" {step}", Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft, QColor("#555566")) |
| app.processEvents() |
| time.sleep(0.3) |
| |
| |
| browser = MoneyPackBrowser() |
| |
| |
| splash.finish(browser) |
| browser.show() |
|
|
| sys.exit(app.exec()) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|