Skip to content

Instantly share code, notes, and snippets.

@patx
Created August 18, 2025 00:55
Show Gist options
  • Select an option

  • Save patx/55f7189b82b499411be603844f8df26e to your computer and use it in GitHub Desktop.

Select an option

Save patx/55f7189b82b499411be603844f8df26e to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# fastPATX (PyQt6)
from __future__ import annotations
import os
import sys
import webbrowser
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from PyQt6 import QtCore, QtWidgets
from PyQt6.QtCore import QUrl, Qt, QTimer, QTemporaryDir
from PyQt6.QtGui import (
QKeySequence,
QAction,
QDesktopServices,
QIcon,
QShortcut,
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import (
QWebEngineProfile,
QWebEngineDownloadRequest,
QWebEngineSettings,
QWebEnginePage,
QWebEngineContextMenuRequest,
QWebEngineNavigationRequest,
)
APP_NAME = "fastPATX"
BOOKMARKS_FILE = Path("fastpatx_faves.txt")
# --- MIME helpers ---
VIEWABLE_MIME_PREFIXES = ("image/", "video/", "audio/")
VIEWABLE_MIME_EXACT = {
"application/pdf",
"text/plain",
"text/html",
"application/xhtml+xml",
"application/xml",
}
# Extensions we treat as binary (trigger download instead of navigate)
BINARY_EXTS = {
".zip", ".xz", ".gz", ".tgz", ".bz2", ".7z", ".rar",
".iso", ".img", ".dmg",
".exe", ".msi", ".apk", ".deb", ".rpm", ".pkg",
".bin", ".tar", ".cab",
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
".psd", ".ai",
}
def is_viewable_mime(mime: str) -> bool:
if not mime:
return False
mime = mime.lower()
if mime in VIEWABLE_MIME_EXACT:
return True
return any(mime.startswith(p) for p in VIEWABLE_MIME_PREFIXES)
def themed_icon(*names: str, fallback: QIcon | None = None) -> QIcon:
for n in names:
ic = QIcon.fromTheme(n)
if not ic.isNull():
return ic
return fallback or QIcon()
# --- Download state enums (Qt6 variants) ---
try:
DL_COMPLETED = QWebEngineDownloadRequest.DownloadState.Completed
DL_CANCELLED = QWebEngineDownloadRequest.DownloadState.Cancelled
DL_INTERRUPTED = QWebEngineDownloadRequest.DownloadState.Interrupted
except AttributeError:
DL_COMPLETED = QWebEngineDownloadRequest.DownloadState.DownloadCompleted
DL_CANCELLED = QWebEngineDownloadRequest.DownloadState.DownloadCancelled
DL_INTERRUPTED = QWebEngineDownloadRequest.DownloadState.DownloadInterrupted
# =================== Downloads Tab ===================
@dataclass
class DownloadRow:
request: QWebEngineDownloadRequest
row: int
filepath: Optional[Path] = None
done: bool = False
ok: bool = False
class DownloadsTab(QtWidgets.QWidget):
def __init__(self, parent: "MainWindow"):
super().__init__(parent)
self.main: MainWindow = parent
self.table = QtWidgets.QTableWidget(0, 6, self)
self.table.setHorizontalHeaderLabels(["File", "Size", "Progress", "Status", "Path", "Actions"])
self.table.horizontalHeader().setStretchLastSection(True)
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
self.table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
self.table.verticalHeader().setVisible(False)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(6, 6, 6, 6)
layout.addWidget(self.table)
self.rows: dict[QWebEngineDownloadRequest, DownloadRow] = {}
@staticmethod
def _fmt_size(n: int) -> str:
if n <= 0:
return "—"
mb = n / (1024 * 1024)
if mb >= 1:
return f"{mb:.2f} MB"
kb = n / 1024
return f"{kb:.0f} KB"
def add_download(self, req: QWebEngineDownloadRequest, filepath: Optional[Path] = None):
r = self.table.rowCount()
self.table.insertRow(r)
name = req.downloadFileName() or "download"
size_text = self._fmt_size(req.totalBytes())
self.table.setItem(r, 0, QtWidgets.QTableWidgetItem(name))
self.table.setItem(r, 1, QtWidgets.QTableWidgetItem(size_text))
prog = QtWidgets.QProgressBar()
prog.setValue(0)
self.table.setCellWidget(r, 2, prog)
self.table.setItem(r, 3, QtWidgets.QTableWidgetItem("Starting…"))
self.table.setItem(r, 4, QtWidgets.QTableWidgetItem(str(filepath) if filepath else "—"))
btn_open = QtWidgets.QPushButton("Open")
btn_open.setEnabled(False)
btn_reveal = QtWidgets.QPushButton("Reveal")
btn_reveal.setEnabled(False)
btn_cancel = QtWidgets.QPushButton("Cancel")
btn_cancel.setEnabled(True)
wrap = QtWidgets.QWidget()
h = QtWidgets.QHBoxLayout(wrap)
h.setContentsMargins(0, 0, 0, 0)
h.addWidget(btn_open)
h.addWidget(btn_reveal)
h.addWidget(btn_cancel)
self.table.setCellWidget(r, 5, wrap)
row = DownloadRow(request=req, row=r, filepath=filepath)
self.rows[req] = row
def update_progress():
rec = req.receivedBytes()
total = req.totalBytes()
pct = int(rec * 100 / total) if total > 0 else 0
prog.setValue(pct)
self.table.item(r, 1).setText(self._fmt_size(total))
self.table.item(r, 3).setText(f"Downloading… {pct}%" if total > 0 else "Downloading…")
def on_state_changed(new_state):
if new_state == DL_COMPLETED:
row.done = True
row.ok = True
prog.setValue(100)
self.table.item(r, 3).setText("Completed")
full = Path(req.downloadDirectory()) / req.downloadFileName()
row.filepath = full
self.table.setItem(r, 4, QtWidgets.QTableWidgetItem(str(full)))
btn_open.setEnabled(True)
btn_reveal.setEnabled(True)
btn_cancel.setEnabled(False)
elif new_state in (DL_CANCELLED, DL_INTERRUPTED):
row.done = True
row.ok = False
self.table.item(r, 3).setText("Cancelled" if new_state == DL_CANCELLED else "Interrupted")
btn_cancel.setEnabled(False)
btn_open.clicked.connect(lambda: self._open_file(self.rows[req].filepath))
btn_reveal.clicked.connect(lambda: self._reveal_file(self.rows[req].filepath))
btn_cancel.clicked.connect(lambda: req.cancel())
req.receivedBytesChanged.connect(update_progress)
req.totalBytesChanged.connect(update_progress)
req.stateChanged.connect(on_state_changed)
update_progress()
self.table.resizeColumnsToContents()
def _open_file(self, path: Optional[Path]):
if not path:
return
QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))
def _reveal_file(self, path: Optional[Path]):
if not path:
return
folder = path.parent
if sys.platform.startswith("linux"):
webbrowser.open(f"file://{folder}")
elif sys.platform == "darwin":
os.system(f'open -R "{path}"')
elif sys.platform.startswith("win"):
os.startfile(folder) # type: ignore
# =================== Browser View + Page ===================
class CustomPage(QWebEnginePage):
"""Interceptors to prevent navigation to likely-binary links and trigger download instead."""
def __init__(self, profile: QWebEngineProfile, parent=None):
super().__init__(profile, parent)
self.navigationRequested.connect(self._on_navigation_requested)
def _on_navigation_requested(self, req: QWebEngineNavigationRequest):
if req.navigationType() != QWebEngineNavigationRequest.NavigationType.LinkClicked:
return
url = req.url()
path = Path(url.path() or "")
ext = path.suffix.lower()
if ext in BINARY_EXTS:
req.reject() # Block navigation immediately
win: MainWindow = self.view().window()
suggested = path.name or "download"
target, _ = QtWidgets.QFileDialog.getSaveFileName(win, "Save File", suggested, "All Files (*)")
if target:
dl = self.download(url, target)
if dl is not None:
win.ensure_downloads_tab()
win._downloads_tab.add_download(dl, Path(target))
win.showDownloads()
# Ensure no navigation occurs
view = self.view()
if view and view.url() == url:
view.stop()
if view.history().canGoBack():
view.back()
else:
view.setUrl(QUrl("about:blank"))
class BrowserView(QWebEngineView):
openLinkInNewTab = QtCore.pyqtSignal(QUrl, bool)
saveLinkAs = QtCore.pyqtSignal(QUrl)
def __init__(self, parent=None):
super().__init__(parent)
profile = QWebEngineProfile.defaultProfile()
self.setPage(CustomPage(profile, self))
def contextMenuEvent(self, event):
req: QWebEngineContextMenuRequest = self.lastContextMenuRequest()
menu = QtWidgets.QMenu(self)
link_url = req.linkUrl()
if link_url.isValid():
act_open = menu.addAction("Open Link in New Tab")
act_open.triggered.connect(lambda: self.openLinkInNewTab.emit(link_url, False))
act_open_bg = menu.addAction("Open Link in Background Tab")
act_open_bg.triggered.connect(lambda: self.openLinkInNewTab.emit(link_url, True))
act_copy = menu.addAction("Copy Link Address")
act_copy.triggered.connect(lambda: QtWidgets.QApplication.clipboard().setText(link_url.toString()))
act_save = menu.addAction("Save Link As…")
act_save.triggered.connect(lambda: self.saveLinkAs.emit(link_url))
menu.addSeparator()
page: QWebEnginePage = self.page()
def add_web_action(web_action, text=None):
act = page.action(web_action)
if text:
act.setText(text)
if act.isEnabled():
menu.addAction(act)
add_web_action(QWebEnginePage.WebAction.Back, "Back")
add_web_action(QWebEnginePage.WebAction.Forward, "Forward")
add_web_action(QWebEnginePage.WebAction.Reload, "Reload")
menu.addSeparator()
add_web_action(QWebEnginePage.WebAction.Copy, "Copy")
add_web_action(QWebEnginePage.WebAction.Cut, "Cut")
add_web_action(QWebEnginePage.WebAction.Paste, "Paste")
add_web_action(QWebEnginePage.WebAction.SelectAll, "Select All")
menu.addSeparator()
win = self.window()
menu.addAction("Downloads", win.showDownloads)
menu.addAction("View Source", win.viewSrc)
menu.addAction("Bookmark Page", win.bookmarkCurrent)
menu.addAction("Your Bookmarks", win.showBookmarks)
menu.addAction("Save Page HTML", win.savePageHtml)
menu.addSeparator()
menu.addAction("About", win.About)
menu.addAction("Help", win.Help)
menu.exec(event.globalPos())
# =================== Tab widgets ===================
class PatxTab(QtWidgets.QWidget):
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
def __init__(self, index: int, parent: "MainWindow" | None = None):
super().__init__(parent)
self.mainUi: MainWindow = parent
self.index = index
self.browser = BrowserView(self)
self.browser.urlChanged.connect(self._updateLineEdit)
self.browser.loadStarted.connect(self._loadStarted)
self.browser.loadFinished.connect(self._loadFinished)
self.browser.titleChanged.connect(self._updateTabTitle)
self.browser.openLinkInNewTab.connect(self._open_link_in_new_tab)
self.browser.saveLinkAs.connect(self._save_link_as)
self.browser.iconChanged.connect(self._icon_changed)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.browser)
self.current_url = QUrl("https://www.google.com/")
self.browser.setUrl(self.current_url)
self._base_title = "(untitled)"
self._spin_idx = 0
self._spinner = QTimer(self)
self._spinner.setInterval(90)
self._spinner.timeout.connect(self._tick_spinner)
def go(self, url: QUrl):
self.current_url = url
self.browser.setUrl(url)
def _loadStarted(self):
self._spin_idx = 0
if not self._spinner.isActive():
self._spinner.start()
def _loadFinished(self, ok: bool):
if self._spinner.isActive():
self._spinner.stop()
self.mainUi.tabs.setTabText(self.index, self._base_title or "(untitled)")
if not ok:
self.browser.setHtml("<p>This page could not be rendered/loaded.</p>")
def _tick_spinner(self):
frame = PatxTab.SPINNER_FRAMES[self._spin_idx % len(PatxTab.SPINNER_FRAMES)]
self._spin_idx += 1
if not self.mainUi.is_plus_tab(self.index):
self.mainUi.tabs.setTabText(self.index, f"{frame} {self._base_title}")
def _updateTabTitle(self, title: str):
self._base_title = title or "(untitled)"
if not self._spinner.isActive():
self.mainUi.tabs.setTabText(self.index, self._base_title)
def _updateLineEdit(self, url: QUrl):
self.current_url = url
if self.index == self.mainUi.tabs.currentIndex():
self.mainUi.address.setText(url.toString())
def _open_link_in_new_tab(self, url: QUrl, background: bool):
self.mainUi.open_url_in_new_tab(url, background=background)
def _save_link_as(self, url: QUrl):
suggested = Path(url.path()).name or "download"
target, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save Link As", suggested, "All Files (*)")
if not target:
return
path = Path(target)
req = self.browser.page().download(url, path.name)
if req is not None:
self.mainUi.ensure_downloads_tab()
self.mainUi._downloads_tab.add_download(req, path)
self.mainUi.showDownloads()
def _icon_changed(self, icon: QIcon):
if self.index == self.mainUi.tabs.currentIndex():
self.mainUi.updateFavicon(icon)
# =================== Main Window ===================
class MainWindow(QtWidgets.QMainWindow):
PLUS_TAB_TEXT = "+"
def __init__(self):
super().__init__()
self.setWindowTitle(APP_NAME)
self.resize(1200, 800)
self.temp_dir = QTemporaryDir()
if not self.temp_dir.isValid():
QtWidgets.QMessageBox.critical(self, APP_NAME, "Failed to create temporary directory.")
central = QtWidgets.QWidget(self)
v = QtWidgets.QVBoxLayout(central)
v.setContentsMargins(6, 6, 6, 6)
v.setSpacing(6)
# Top bar
top = QtWidgets.QHBoxLayout()
top.setSpacing(6)
style = self.style()
icon_back = style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ArrowBack)
icon_forward = style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ArrowForward)
icon_home = style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_DirHomeIcon)
icon_globe = themed_icon("internet-web-browser", fallback=style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_DesktopIcon))
self.btnBack = QtWidgets.QToolButton()
self.btnBack.setIcon(icon_back)
self.btnBack.clicked.connect(self.goBack)
self.btnForward = QtWidgets.QToolButton()
self.btnForward.setIcon(icon_forward)
self.btnForward.clicked.connect(self.goForward)
self.btnHome = QtWidgets.QToolButton()
self.btnHome.setIcon(icon_home)
self.btnHome.clicked.connect(self.goHome)
self.address = QtWidgets.QLineEdit()
self.address.returnPressed.connect(self.addressGo)
self.addrFaviconAct = self.address.addAction(icon_globe, QtWidgets.QLineEdit.ActionPosition.LeadingPosition)
self.addrGoAct = self.address.addAction(icon_forward, QtWidgets.QLineEdit.ActionPosition.TrailingPosition)
self.addrGoAct.triggered.connect(self.addressGo)
top.addWidget(self.btnBack)
top.addWidget(self.btnForward)
top.addWidget(self.btnHome)
top.addWidget(self.address, 1)
v.addLayout(top)
# Tabs
self.tabs = QtWidgets.QTabWidget()
self.tabs.setDocumentMode(True)
self.tabs.setTabsClosable(True)
self.tabs.tabCloseRequested.connect(self.closeTab)
self.tabs.tabBarClicked.connect(self._on_tabbar_clicked)
self.tabs.currentChanged.connect(self._tabChanged)
v.addWidget(self.tabs)
self.setCentralWidget(central)
# Downloads tab handle
self._downloads_tab: Optional[DownloadsTab] = None
self._downloads_tab_index: int = -1
# First real tab and downloads tab
self.newTab()
self.ensure_downloads_tab()
self.ensure_plus_tab()
# Ensure close buttons exist then sync visibility
self._force_create_close_buttons()
self.update_close_buttons()
# Profile & downloads
profile = QWebEngineProfile.defaultProfile()
try:
profile.settings().setAttribute(QWebEngineSettings.WebAttribute.PdfViewerEnabled, True)
except Exception:
pass
profile.downloadRequested.connect(self.on_downloadRequested)
QtWidgets.QApplication.instance().setWindowIcon(themed_icon("applications-internet"))
# Shortcuts
QShortcut(QKeySequence("Ctrl+T"), self, activated=self.newTab)
QShortcut(QKeySequence("Ctrl+W"), self, activated=lambda: self.closeTab(self.tabs.currentIndex()))
QShortcut(QKeySequence("Alt+Left"), self, activated=self.goBack)
QShortcut(QKeySequence("Alt+Right"), self, activated=self.goForward)
QShortcut(QKeySequence("Ctrl+H"), self, activated=self.goHome)
QShortcut(QKeySequence("Ctrl+U"), self, activated=self.viewSrc)
QShortcut(QKeySequence("Ctrl+D"), self, activated=self.savePageHtml)
# ---- Close button logic ----
def _force_create_close_buttons(self):
self.tabs.setTabsClosable(False)
self.tabs.setTabsClosable(True)
def browser_tab_count(self) -> int:
return sum(1 for i in range(self.tabs.count()) if isinstance(self.tabs.widget(i), PatxTab))
def update_close_buttons(self):
"""Show 'X' on browser tabs if >1 browser tab; always on downloads; hide on '+' tab."""
tb = self.tabs.tabBar()
bcount = self.browser_tab_count()
for i in range(self.tabs.count()):
is_plus = self.is_plus_tab(i)
is_dls = i == self._downloads_tab_index
btn = tb.tabButton(i, QtWidgets.QTabBar.ButtonPosition.RightSide)
if btn:
btn.setVisible(not is_plus and (is_dls or bcount > 1))
# ---- Downloads tab helpers ----
def ensure_downloads_tab(self):
if self._downloads_tab is None or self._downloads_tab_index < 0 or self._downloads_tab_index >= self.tabs.count():
self._downloads_tab = DownloadsTab(self)
insert_at = self.tabs.count()
if self.tabs.count() and self.is_plus_tab(self.tabs.count() - 1):
insert_at -= 1
self._downloads_tab_index = self.tabs.insertTab(insert_at, self._downloads_tab, "Downloads")
self._force_create_close_buttons()
self.update_close_buttons()
return self._downloads_tab_index
def showDownloads(self):
idx = self.ensure_downloads_tab()
self.tabs.setCurrentIndex(idx)
self.update_close_buttons()
# ---- "+" tab helpers ----
def ensure_plus_tab(self):
if self.tabs.count() == 0 or self.tabs.tabText(self.tabs.count() - 1) != self.PLUS_TAB_TEXT:
dummy = QtWidgets.QWidget()
idx = self.tabs.addTab(dummy, self.PLUS_TAB_TEXT)
self.tabs.tabBar().setTabButton(idx, QtWidgets.QTabBar.ButtonPosition.RightSide, None)
def is_plus_tab(self, index: int) -> bool:
return 0 <= index < self.tabs.count() and self.tabs.tabText(index) == self.PLUS_TAB_TEXT
def _on_tabbar_clicked(self, index: int):
if self.is_plus_tab(index):
self.newTab()
# ---- Tabs ----
def newTab(self, url: QUrl | None = None, background: bool = False):
insert_at = self.tabs.count()
if self.tabs.count() > 0 and self.is_plus_tab(self.tabs.count() - 1):
insert_at -= 1
tab = PatxTab(insert_at, self)
idx = self.tabs.insertTab(insert_at, tab, "(untitled)")
if not background:
self.tabs.setCurrentIndex(idx)
if url:
tab.go(url)
self.ensure_plus_tab()
self._force_create_close_buttons()
self.update_close_buttons()
def closeTab(self, index: int):
if self.is_plus_tab(index):
return
w = self.tabs.widget(index)
if isinstance(w, PatxTab) and self.browser_tab_count() <= 1:
return
if w is self._downloads_tab:
self._downloads_tab = None
self._downloads_tab_index = -1
if w:
self.tabs.removeTab(index)
w.deleteLater()
self.ensure_plus_tab()
self._force_create_close_buttons()
self.update_close_buttons()
def _tabChanged(self, idx: int):
w = self.tabs.widget(idx)
if isinstance(w, PatxTab):
self.address.setText(w.current_url.toString())
self.updateFavicon(w.browser.icon())
else:
self.address.clear()
self.updateFavicon(QIcon())
self._force_create_close_buttons()
self.update_close_buttons()
# ---- Convenience ----
def currentBrowser(self) -> Optional["BrowserView"]:
w = self.tabs.currentWidget()
return w.browser if isinstance(w, PatxTab) else None
# ---- Navigation ----
def goBack(self):
b = self.currentBrowser()
if b:
b.back()
def goForward(self):
b = self.currentBrowser()
if b:
b.forward()
def goHome(self):
b = self.currentBrowser()
if b:
b.setUrl(QUrl("https://www.google.com/"))
def addressGo(self):
raw = self.address.text().strip()
if not raw:
return
if "://" not in raw and "." not in raw:
url = QUrl(f"https://www.google.com/search?hl=en&safe=off&q={QUrl.fromPercentEncoding(raw.encode())}")
else:
url = QUrl.fromUserInput(raw)
w = self.tabs.currentWidget()
if isinstance(w, PatxTab):
w.go(url)
else:
self.newTab(url)
def open_url_in_new_tab(self, url: QUrl, background: bool = False):
self.newTab(url, background=background)
def updateFavicon(self, icon: QIcon):
if icon.isNull():
icon = themed_icon("internet-web-browser", fallback=self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_DesktopIcon))
self.addrFaviconAct.setIcon(icon)
# ---- App actions (invoked from context menu) ----
def viewSrc(self):
b = self.currentBrowser()
if not b:
return
def show(html: str):
dlg = QtWidgets.QDialog(self)
dlg.setWindowTitle("Page Source")
txt = QtWidgets.QPlainTextEdit(dlg)
txt.setPlainText(html)
txt.setReadOnly(True)
ok = QtWidgets.QPushButton("&OK", dlg)
ok.clicked.connect(dlg.accept)
lay = QtWidgets.QVBoxLayout(dlg)
lay.addWidget(txt)
lay.addWidget(ok)
dlg.resize(900, 600)
dlg.exec()
b.page().toHtml(show)
def bookmarkCurrent(self):
url_text = self.address.text().strip()
if not url_text:
return
existing = BOOKMARKS_FILE.read_text(encoding="utf-8") if BOOKMARKS_FILE.exists() else ""
link = f'<a href="{QUrl.fromUserInput(url_text).toString()}">{url_text}</a><br>'
BOOKMARKS_FILE.write_text(existing + link, encoding="utf-8")
QtWidgets.QMessageBox.information(self, APP_NAME, "The page has been bookmarked.")
def showBookmarks(self):
html = BOOKMARKS_FILE.read_text(encoding="utf-8") if BOOKMARKS_FILE.exists() else "<p>No bookmarks yet.</p>"
self.open_url_in_new_tab(QUrl("about:blank"))
w = self.tabs.currentWidget()
if isinstance(w, PatxTab):
w.browser.setHtml(html)
self._force_create_close_buttons()
self.update_close_buttons()
def savePageHtml(self):
b = self.currentBrowser()
if not b:
return
dest, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save Page HTML", "page.html", "HTML Files (*.html);;All Files (*)")
if not dest:
return
def save(html: str):
try:
Path(dest).write_text(html, encoding="utf-8")
QtWidgets.QMessageBox.information(self, APP_NAME, f"Saved page HTML to:\n{dest}")
except Exception as e:
QtWidgets.QMessageBox.critical(self, "Error", f"Failed to save:\n{e}")
b.page().toHtml(save)
def About(self):
QtWidgets.QMessageBox.about(
self,
APP_NAME,
('<p><b>fastPATX</b></p>'
'<p>Owner: Harrison Erd</p>'
'<p>License: Zlib</p>'
'<p>PyQt6 + Qt WebEngine</p>')
)
def Help(self):
QtWidgets.QMessageBox.information(
self,
APP_NAME,
("Shortcuts:\n"
" Back: Alt+Left Forward: Alt+Right New Tab: Ctrl+T Close Tab: Ctrl+W\n"
" Home: Ctrl+H View Source: Ctrl+U Save HTML: Ctrl+D")
)
# ---- Downloads (profile-level) ----
@QtCore.pyqtSlot(QWebEngineDownloadRequest)
def on_downloadRequested(self, download: QWebEngineDownloadRequest):
mime = (download.mimeType() or "").lower()
url = download.url()
viewable = is_viewable_mime(mime)
suggested = download.downloadFileName() or Path(url.path()).name or "download"
# Prevent navigation to the download URL
cur = self.currentBrowser()
if cur and cur.url() == url:
cur.stop()
if cur.history().canGoBack():
cur.back()
else:
cur.setUrl(QUrl("about:blank"))
if viewable:
msg = QtWidgets.QMessageBox(self)
msg.setText(f"The server is offering to download \"{suggested}\". Would you like to open it inline or save it?")
open_btn = msg.addButton("Open", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
save_btn = msg.addButton("Save", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
cancel_btn = msg.addButton("Cancel", QtWidgets.QMessageBox.ButtonRole.RejectRole)
msg.exec()
if msg.clickedButton() == cancel_btn:
download.cancel()
return
if msg.clickedButton() == open_btn:
download.setDownloadDirectory(self.temp_dir.path())
download.accept()
def on_state_changed(state):
if state == DL_COMPLETED:
full = Path(download.downloadDirectory()) / download.downloadFileName()
file_url = QUrl.fromLocalFile(str(full))
self.open_url_in_new_tab(file_url)
download.stateChanged.connect(on_state_changed)
return
target, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save File", suggested, "All Files (*)")
if not target:
download.cancel()
return
chosen_path = Path(target)
download.setDownloadDirectory(str(chosen_path.parent))
download.setDownloadFileName(chosen_path.name)
self.ensure_downloads_tab()
self._downloads_tab.add_download(download, chosen_path)
self.showDownloads()
download.accept()
# =================== App bootstrap ===================
def main():
QtWidgets.QApplication.setApplicationName(APP_NAME)
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment