MoneyPack-Security-Suite / moneypack_browser_final.py
MoneyPack's picture
Fix: add QRect to QtCore imports
4d51349 verified
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
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)
# Chrome-like user agent so sites don't block us
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 BLOCKER
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# BOOKMARKS
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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 - Pure dark, simple, fast
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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>"""
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# DARK PAGE CLASS - handles fullscreen, popups, certs
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class DarkWebPage(QWebEnginePage):
"""Page with dark background, fullscreen support, popup handling."""
# Signal to tell main window to open a new tab
open_in_new_tab = None # set by BrowserTab
def __init__(self, profile=None, parent=None):
if profile:
super().__init__(profile, parent)
else:
super().__init__(parent)
self.setBackgroundColor(QColor("#08080e"))
# Handle fullscreen requests (YouTube etc)
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."""
# Create a new page for the popup
new_page = DarkWebPage(self.profile(), self.parent())
# Tell the main window to create a tab for this page
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
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# BROWSER TAB
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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)
# Configure ALL settings for real-world browsing
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)
# Link hover shows URL in status bar
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"))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# AI SIDEBAR
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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
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)
# Web view
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]))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# MAIN BROWSER WINDOW
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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)
# Set up persistent profile with proper user-agent
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
)
# Ad blocker - must keep reference alive
self._ad_blocker = AdBlockInterceptor(self)
self._profile.setUrlRequestInterceptor(self._ad_blocker)
# Download handler
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()
# First tab
self._new_tab()
# ── Menu Bar ──
def _create_menubar(self):
mb = self.menuBar()
# File
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")
# Navigate
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
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
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 features menu
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
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
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)
# Bookmarks
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
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
# ── UI ──
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
browser_area = QWidget()
browser_layout = QVBoxLayout(browser_area)
browser_layout.setContentsMargins(0, 0, 0, 0)
browser_layout.setSpacing(0)
# Toolbar
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)
# Nav buttons
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)
# URL bar
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)
# Bookmark button
bm_btn = QPushButton("\u2606")
bm_btn.setToolTip("Bookmark (Ctrl+D)")
bm_btn.clicked.connect(self._bookmark)
tl.addWidget(bm_btn)
# AI button
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)
# Ad block counter
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)
# Tab widget
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)
# AI Sidebar (hidden)
self._ai_sidebar = AISidebar(self)
self._ai_sidebar.setVisible(False)
main_layout.addWidget(self._ai_sidebar)
# Status bar
self.statusBar().showMessage("Ready")
# Timer for ad count
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)
# ── Tab Management ──
def _new_tab(self, url=None):
# Use private profile or default
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()
# Connect signals
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)
# Replace the tab's page with the one created by createWindow
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)
# ── Navigation ──
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()
# ── View ──
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)
# ── Tools ──
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}")
# ── Video ──
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")
# ── SPLIT VIEW (from Vivaldi) ──
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
# Get current tab and next tab
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
# Create split window
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)
# Create two new web views
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)
# Add as new tab
idx = self._tabs.addTab(split_widget, "Split View")
self._tabs.setCurrentIndex(idx)
self.statusBar().showMessage("Split view opened")
# ── COMMAND PALETTE (from Arc) ──
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)
# Populate with all items
all_items = []
# Open tabs
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))
# Bookmarks
for bm in BookmarkManager.load():
all_items.append(("bookmark", f"Bookmark: {bm['title']}", bm['url'], None))
# Actions
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()
# ── READER MODE (from Firefox) ──
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")
# ── SCREENSHOT (from Edge) ──
def _screenshot(self):
"""Take screenshot of current page."""
w = self._current_web()
if not w:
return
# Capture the page
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:
# Grab the widget
pixmap = w.grab()
pixmap.save(save_path)
self.statusBar().showMessage(f"Screenshot saved: {os.path.basename(save_path)}")
# ── FORCE DARK MODE (from Brave/Vivaldi) ──
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")
# ── NOTES PANEL (from Edge Drop) ──
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...")
# Load existing notes
if notes_file.exists():
text_edit.setPlainText(notes_file.read_text())
layout.addWidget(text_edit)
# Auto-save on close
def save_notes():
notes_file.write_text(text_edit.toPlainText())
dialog.finished.connect(lambda: save_notes())
dialog.exec()
# ── FOCUS MODE (from Min browser) ──
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")
# ══════════════════════════════════════════════════════
# 10 MORE KILLER FEATURES (from 10 more browsers)
# ══════════════════════════════════════════════════════
# ── NEW IDENTITY (from Tor) ──
def _new_identity(self):
"""Wipe all session data instantly - fresh start."""
profile = QWebEngineProfile.defaultProfile()
profile.cookieStore().deleteAllCookies()
profile.clearHttpCache()
profile.clearAllVisitedLinks()
# Close all tabs and open fresh
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")
# ── AUTO-FILL (from Maxthon Magic Fill) ──
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}")
# Open it
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")
# ── FINGERPRINT BLOCKER (from LibreWolf) ──
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")
# ── TURBO MODE (from Yandex) ──
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")
# ── MOUSE GESTURES (from Sleipnir) ──
# Note: Real mouse gestures need event tracking - we'll add gesture-like quick actions
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()
# ── WEB APP MODE (from Epiphany/GNOME Web) ──
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"
# Create minimal window
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}")
# ── WORKSPACES (from Floorp) ──
def _switch_workspace(self):
"""Switch between tab workspaces."""
from PyQt6.QtWidgets import QDialog, QListWidget, QListWidgetItem
# Save current workspace
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:
# Close current tabs
while self._tabs.count() > 0:
w = self._tabs.widget(0)
self._tabs.removeTab(0)
w.deleteLater()
# Open workspace tabs
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()
# ── SESSION MANAGER (from Falkon) ──
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)")
# ── TAB SUSPENDER (from Sidekick) ──
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"
# Replace with lightweight placeholder
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}")
# ══════════════════════════════════════════════════════
# 10 MORE FEATURES (batch 3 - browsers 21-30)
# ══════════════════════════════════════════════════════
# ── AUTO-HIDE TOOLBAR (from Zen Browser) ──
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")
# ── FIRE BUTTON (from DuckDuckGo Browser) ──
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")
# ── VIDEO ASSISTANT (from Samsung Internet) ──
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)})()""")
# ── USERSCRIPT LOADER (from Kiwi Browser) ──
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)}")
# ── PAGE INFO (from Pale Moon) ──
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)
# ── VIEW SOURCE (from Lunascape) ──
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('&','&amp;').replace('<','&lt;').replace('>','&gt;')
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")
# ── FILE BROWSER (from Konqueror) ──
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")
# ── LINK HINTS (from Surf/Vimium) ──
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")
# ── HISTORY VIEW (from Nyxt) ──
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")
# ══════════════════════════════════════════════════════
# 10 MORE FEATURES (batch 4 - browsers 31-40)
# ══════════════════════════════════════════════════════
# ── URL NOTES (from Otter Browser) ──
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()
# ── FORCE HTTPS (from Bromite) ──
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")
# ── CONNECTION MONITOR (from Ungoogled Chromium concept) ──
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)
# ── SSL CHECK (from Comodo Dragon) ──
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!")
# ── PAGE ANNOTATIONS (from Dissenter) ──
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);
}})()""")
# ── QUICK LAUNCH BAR (from Pale Moon) ──
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()
# ── CUSTOM BLOCK RULES (from Otter content blocker) ──
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())
# Add to ad blocker
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()
# ── INJECT CUSTOM CSS (from Basilisk/Stylish) ──
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")
# ── TAB AUTO-EXPIRE (from Fennec) ──
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)
# ── ISOLATED TAB (from Ghost Browser) ──
def _isolated_tab(self):
"""Open a tab with its own isolated session (separate cookies/login)."""
# Create a unique profile for this tab
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")
# ══════════════════════════════════════════════════════
# 10 MORE FEATURES (batch 5 - browsers 41-50)
# ══════════════════════════════════════════════════════
# ── RESPONSIVE PREVIEW (from Polypane) ──
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()
# ── DEVICE EMULATION (from Blisk) ──
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
# ── PAGE LOAD TIMER (from Ladybird concept) ──
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)
# ── TAB OVERVIEW GRID (from Stack browser) ──
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()
# ── CLIPBOARD HISTORY (from Sigma browser) ──
def _clipboard_history(self):
"""Show clipboard and allow paste from history."""
clip = QApplication.clipboard()
current = clip.text()
# Load history
clip_file = DATA_DIR / "clipboard.json"
history = []
if clip_file.exists():
try: history = json.loads(clip_file.read_text())
except: pass
# Add current if new
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()
# ── TEXT TO SPEECH (from Beam browser) ──
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)")
# ── QR CODE SHARE (from Opera Flow) ──
def _share_qr(self):
"""Generate QR code of current URL to share."""
w = self._current_web()
if not w: return
url = w.url().toString()
# Generate QR as an HTML page using a free API
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")
# ── PRODUCTIVITY TRACKER (from Ecosia concept) ──
def _productivity_stats(self):
"""Show browsing stats for this session."""
tabs_open = self._tabs.count()
ads_blocked = self._ad_blocker.blocked_count
# Count bookmarks
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")
# ── SCROLL TO TOP/BOTTOM (from mobile browsers) ──
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'})")
# ══════════════════════════════════════════════════════
# FINAL 15 - #2 features from top browsers
# ══════════════════════════════════════════════════════
# ── TAB PINNING (from Chrome) ──
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"):
# Unpin
self._tabs.setTabText(idx, title[2:])
self.statusBar().showMessage("Tab unpinned")
else:
# Pin - move to front
self._tabs.tabBar().moveTab(idx, 0)
self._tabs.setTabText(0, f"\U0001f4cc{title[:8]}")
self.statusBar().showMessage("Tab pinned")
# ── TAB MUTE (from Vivaldi) ──
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")
# ── PERFORMANCE SCORE (from Brave) ──
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
# ── SAVE FOR LATER (from Firefox Pocket) ──
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()}")
# ── VIEW READING LIST ──
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")
# ── DISABLE JAVASCRIPT (from Tor) ──
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'}")
# ── TAB SEARCH (from Sidekick) ──
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")
# ── HIDE EMAIL (from DuckDuckGo Email Protection concept) ──
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)")
# Copy to clipboard
QApplication.clipboard().setText(fake_email)
self.statusBar().showMessage(f"Copied to clipboard: {fake_email}")
# ── COLOR PICKER (from Polypane) ──
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")
# ── NARROW WINDOW / ONE-HAND MODE (from Samsung) ──
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")
# ── ZOOM MEMORY PER-SITE (from Vivaldi) ──
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}")
# ── MULTI-ACCOUNT INDICATOR (from Ghost) ──
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]}...")
# ── About ──
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)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ENTRY POINT
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def main():
app = QApplication(sys.argv)
app.setApplicationName(APP_NAME)
app.setFont(QFont("Segoe UI", 10))
# Force dark palette at Qt level (absolute prevention of white)
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)
# ── ANIMATED SPLASH SCREEN ──
splash_pix = QPixmap(520, 320)
splash_pix.fill(QColor("#08080e"))
painter = QPainter(splash_pix)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# Background gradient
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 circle behind logo
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)
# Brand logo text
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")
# Subtitle
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")
# Version
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")
# Loading bar background
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)
# Loading bar fill (static for splash, we'll animate separately)
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)
# Loading text
painter.setPen(QColor("#555566"))
painter.setFont(QFont("Segoe UI", 9))
painter.drawText(QRect(0, 240, 520, 20), Qt.AlignmentFlag.AlignCenter, "Loading browser engine...")
# Bottom decoration line
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)
# Footer
painter.setPen(QColor("#222230"))
painter.setFont(QFont("Segoe UI", 7))
painter.drawText(QRect(0, 298, 520, 15), Qt.AlignmentFlag.AlignCenter, "moneypack.dev")
painter.end()
# Show splash
from PyQt6.QtWidgets import QSplashScreen
splash = QSplashScreen(splash_pix)
splash.setWindowFlags(Qt.WindowType.SplashScreen | Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
splash.show()
app.processEvents()
# Animate the splash with progress updates
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)
# Create browser
browser = MoneyPackBrowser()
# Close splash and show browser
splash.finish(browser)
browser.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()