updates
This commit is contained in:
parent
9a8c6c5c01
commit
b0eea9d62e
10 changed files with 591 additions and 183 deletions
109
gfd/__init__.py
109
gfd/__init__.py
|
|
@ -1,108 +1 @@
|
||||||
from PySide6 import QtWidgets, QtCore
|
from .app import run
|
||||||
import gfd.i18n as i18n
|
|
||||||
from gfd import sfd
|
|
||||||
|
|
||||||
|
|
||||||
class GFDWidget(QtWidgets.QWidget):
|
|
||||||
def __init__(self, tr):
|
|
||||||
super().__init__()
|
|
||||||
self.tr = tr
|
|
||||||
self.setWindowTitle(self.tr("window_title"))
|
|
||||||
self.resize(400, 220)
|
|
||||||
|
|
||||||
self.label = QtWidgets.QLabel(
|
|
||||||
self.tr("hello_world"),
|
|
||||||
alignment=QtCore.Qt.AlignmentFlag.AlignCenter,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.combo = QtWidgets.QComboBox()
|
|
||||||
self.combo.addItem(self.tr("loading_installers") + "…")
|
|
||||||
|
|
||||||
self.refresh_btn = QtWidgets.QPushButton("⟳ " + self.tr("refresh"))
|
|
||||||
self.refresh_btn.clicked.connect(self.reload_installers)
|
|
||||||
|
|
||||||
combo_layout = QtWidgets.QHBoxLayout()
|
|
||||||
combo_layout.addWidget(self.combo)
|
|
||||||
combo_layout.addWidget(self.refresh_btn)
|
|
||||||
|
|
||||||
self.md5_label = QtWidgets.QLabel(
|
|
||||||
"", alignment=QtCore.Qt.AlignmentFlag.AlignCenter
|
|
||||||
)
|
|
||||||
|
|
||||||
layout = QtWidgets.QVBoxLayout(self)
|
|
||||||
layout.addWidget(self.label)
|
|
||||||
layout.addLayout(combo_layout)
|
|
||||||
layout.addWidget(self.md5_label)
|
|
||||||
|
|
||||||
self.combo.currentIndexChanged.connect(self.on_selection_changed)
|
|
||||||
|
|
||||||
QtCore.QTimer.singleShot(0, self.load_installers_async)
|
|
||||||
|
|
||||||
def reload_installers(self):
|
|
||||||
"""Triggered by the refresh button."""
|
|
||||||
self.refresh_btn.setEnabled(False)
|
|
||||||
self.combo.clear()
|
|
||||||
self.combo.addItem(self.tr("loading_installers") + "…")
|
|
||||||
self.load_installers_async()
|
|
||||||
|
|
||||||
def load_installers_async(self):
|
|
||||||
"""Fetch installer options without freezing the UI."""
|
|
||||||
self.thread = QtCore.QThread()
|
|
||||||
self.worker = SFDWorker()
|
|
||||||
self.worker.moveToThread(self.thread)
|
|
||||||
|
|
||||||
self.thread.started.connect(self.worker.run)
|
|
||||||
self.worker.finished.connect(self.thread.quit)
|
|
||||||
self.worker.finished.connect(self.worker.deleteLater)
|
|
||||||
self.thread.finished.connect(self.thread.deleteLater)
|
|
||||||
self.worker.result_ready.connect(self.populate_installers)
|
|
||||||
|
|
||||||
self.thread.start()
|
|
||||||
|
|
||||||
@QtCore.Slot(list)
|
|
||||||
def populate_installers(self, installers):
|
|
||||||
self.refresh_btn.setEnabled(True)
|
|
||||||
self.combo.clear()
|
|
||||||
|
|
||||||
if not installers:
|
|
||||||
self.combo.addItem(self.tr("no_installers_found"))
|
|
||||||
self.md5_label.setText("")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.installers = installers
|
|
||||||
for item in installers:
|
|
||||||
self.combo.addItem(item["name"], item["md5"])
|
|
||||||
|
|
||||||
self.combo.setCurrentIndex(0)
|
|
||||||
self.on_selection_changed(0)
|
|
||||||
|
|
||||||
@QtCore.Slot(int)
|
|
||||||
def on_selection_changed(self, index):
|
|
||||||
if not hasattr(self, "installers") or not self.installers:
|
|
||||||
return
|
|
||||||
md5 = self.combo.currentData()
|
|
||||||
name = self.combo.currentText()
|
|
||||||
self.md5_label.setText(f"<b>{name}</b><br>MD5: <code>{md5 or 'N/A'}</code>")
|
|
||||||
|
|
||||||
|
|
||||||
class SFDWorker(QtCore.QObject):
|
|
||||||
"""Background worker to fetch installers safely."""
|
|
||||||
|
|
||||||
finished = QtCore.Signal()
|
|
||||||
result_ready = QtCore.Signal(list)
|
|
||||||
|
|
||||||
@QtCore.Slot()
|
|
||||||
def run(self):
|
|
||||||
data = sfd.fetchInstallerOptions()
|
|
||||||
self.result_ready.emit(data)
|
|
||||||
self.finished.emit()
|
|
||||||
|
|
||||||
|
|
||||||
def run(lang="es"):
|
|
||||||
import sys
|
|
||||||
|
|
||||||
app = QtWidgets.QApplication(sys.argv)
|
|
||||||
tr = i18n.Translator(lang)
|
|
||||||
w = GFDWidget(tr)
|
|
||||||
w.show()
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
|
||||||
13
gfd/app.py
Normal file
13
gfd/app.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from PySide6 import QtWidgets
|
||||||
|
from gfd.ui.main_window import GFDWidget
|
||||||
|
from gfd.i18n import Translator
|
||||||
|
|
||||||
|
|
||||||
|
def run(lang="es"):
|
||||||
|
import sys
|
||||||
|
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
tr = Translator(lang)
|
||||||
|
window = GFDWidget(tr)
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
98
gfd/core/installers.py
Normal file
98
gfd/core/installers.py
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
|
from gfd.core import sfd
|
||||||
|
|
||||||
|
SUPPORTED_INSTALLERS = [
|
||||||
|
{
|
||||||
|
"os_type": "ubuntu24",
|
||||||
|
"name": "Usuarios Linux - Ubuntu 24.04 LTS (DEB 64bits) - 78 MB",
|
||||||
|
"md5": "bdc871e15f2096f930b285f0ed799aa0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"os_type": "debian",
|
||||||
|
"name": "Usuarios Linux - Ubuntu 24.04 LTS (DEB 64bits) - 78 MB",
|
||||||
|
"md5": "bdc871e15f2096f930b285f0ed799aa0",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize(t):
|
||||||
|
return re.sub(r"\s+", " ", unicodedata.normalize("NFKD", t).lower()).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch(max_attempts=3, delay=2):
|
||||||
|
for i in range(max_attempts):
|
||||||
|
data = sfd.fetchInstallerOptions()
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
if i < max_attempts - 1:
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_installers(os_type, installers=None):
|
||||||
|
"""
|
||||||
|
Return a list of confirmed available installers for the given OS type.
|
||||||
|
|
||||||
|
If an installer is listed, it means that for a given OS type:
|
||||||
|
- There is a supported installation routine.
|
||||||
|
- There is a downloadable archive from Soporte Firma Digital.
|
||||||
|
"""
|
||||||
|
src = installers or SUPPORTED_INSTALLERS
|
||||||
|
locals_ = [i for i in src if i["os_type"] == os_type]
|
||||||
|
|
||||||
|
if not locals_:
|
||||||
|
return []
|
||||||
|
|
||||||
|
remote = _fetch()
|
||||||
|
|
||||||
|
if not remote:
|
||||||
|
return []
|
||||||
|
|
||||||
|
rmap = {_normalize(r["name"]): r.get("md5") for r in remote}
|
||||||
|
confirmed = []
|
||||||
|
|
||||||
|
for local in locals_:
|
||||||
|
lname, lmd5 = (
|
||||||
|
_normalize(local["name"]),
|
||||||
|
(local.get("md5") or "").lower() or None,
|
||||||
|
)
|
||||||
|
rmd5 = rmap.get(lname)
|
||||||
|
|
||||||
|
if (lmd5 == rmd5) or (lmd5 is None and rmd5 is None):
|
||||||
|
confirmed.append((local["name"], lmd5))
|
||||||
|
|
||||||
|
return confirmed
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Routine to check for installed version
|
||||||
|
def get_installed_version():
|
||||||
|
"""
|
||||||
|
Return the current installed version data, or empty if not installed.
|
||||||
|
"""
|
||||||
|
return [] # Return always empty for now
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os_type = "debian"
|
||||||
|
print(f"=== TEST: {os_type.upper()} Installer Validation ===")
|
||||||
|
confirmed = get_available_installers(os_type)
|
||||||
|
print(
|
||||||
|
f"{len(confirmed)} confirmed installer(s):"
|
||||||
|
if confirmed
|
||||||
|
else "No confirmed installers found."
|
||||||
|
)
|
||||||
|
|
||||||
|
for n, m in confirmed:
|
||||||
|
print(f" - {n} (MD5={m or 'N/A'})")
|
||||||
|
|
||||||
|
installed = get_installed_version()
|
||||||
|
|
||||||
|
if not installed:
|
||||||
|
print("Status: NOT INSTALLED")
|
||||||
|
else:
|
||||||
|
print(f"Installed: {installed[0]} MD5={installed[1]}")
|
||||||
74
gfd/core/osinfo.py
Normal file
74
gfd/core/osinfo.py
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _read_os_release():
|
||||||
|
"""Read and parse /etc/os-release or /usr/lib/os-release if available."""
|
||||||
|
paths = [Path("/etc/os-release"), Path("/usr/lib/os-release")]
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
if path.exists():
|
||||||
|
try:
|
||||||
|
for line in path.read_text(encoding="utf-8").splitlines():
|
||||||
|
if "=" in line:
|
||||||
|
key, val = line.split("=", 1)
|
||||||
|
data[key.strip()] = val.strip().strip('"')
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_os_type():
|
||||||
|
"""
|
||||||
|
Detect and return a simple OS type string.
|
||||||
|
|
||||||
|
Returns one of:
|
||||||
|
'macos', 'windows', 'ubuntu24', 'ubuntu22', 'ubuntu20',
|
||||||
|
'debian', 'arch', 'rpm', or None if unsupported.
|
||||||
|
"""
|
||||||
|
system = platform.system().lower()
|
||||||
|
os_type = None
|
||||||
|
|
||||||
|
if system == "darwin":
|
||||||
|
os_type = "macos"
|
||||||
|
|
||||||
|
elif system == "windows":
|
||||||
|
os_type = "windows"
|
||||||
|
|
||||||
|
elif system == "linux":
|
||||||
|
info = _read_os_release()
|
||||||
|
id_name = info.get("ID", "").lower()
|
||||||
|
version_id = info.get("VERSION_ID", "")
|
||||||
|
|
||||||
|
# Ubuntu
|
||||||
|
if id_name == "ubuntu":
|
||||||
|
if version_id.startswith("24"):
|
||||||
|
os_type = "ubuntu24"
|
||||||
|
elif version_id.startswith("22"):
|
||||||
|
os_type = "ubuntu22"
|
||||||
|
elif version_id.startswith("20"):
|
||||||
|
os_type = "ubuntu20"
|
||||||
|
|
||||||
|
# Debian (11 or newer supported)
|
||||||
|
elif id_name == "debian":
|
||||||
|
try:
|
||||||
|
version_num = int(re.findall(r"\d+", version_id or "0")[0])
|
||||||
|
except IndexError:
|
||||||
|
version_num = 0
|
||||||
|
if version_num >= 11:
|
||||||
|
os_type = "debian"
|
||||||
|
|
||||||
|
# Arch / RPM-based
|
||||||
|
elif "arch" in id_name:
|
||||||
|
os_type = "arch"
|
||||||
|
elif id_name in ["fedora", "rhel", "centos", "rocky", "alma", "opensuse"]:
|
||||||
|
os_type = "rpm"
|
||||||
|
|
||||||
|
return os_type
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Detected OS type:", get_os_type() or "NOT SUPPORTED")
|
||||||
52
gfd/core/routines.py
Normal file
52
gfd/core/routines.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""
|
||||||
|
routines.py - OS-specific installer routines
|
||||||
|
|
||||||
|
Defines a registry of install routines by OS type.
|
||||||
|
If the current OS is supported and has an associated installer,
|
||||||
|
the matching routine can be executed to perform installation steps.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from gfd.core.osinfo import get_os_type
|
||||||
|
|
||||||
|
|
||||||
|
def install_ubuntu24():
|
||||||
|
"""Simulated install routine for Ubuntu 24.04."""
|
||||||
|
print("Running Ubuntu 24.04 (DEB) installation routine...")
|
||||||
|
|
||||||
|
|
||||||
|
def install_debian():
|
||||||
|
"""Simulated install routine for Debian."""
|
||||||
|
print("Running Debian installation routine (using Ubuntu 24 package)...")
|
||||||
|
|
||||||
|
|
||||||
|
SUPPORTED_ROUTINES = {
|
||||||
|
"ubuntu24": install_ubuntu24,
|
||||||
|
"debian": install_debian,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_install_routine(os_type=None):
|
||||||
|
"""
|
||||||
|
Run the appropriate install routine for the given OS type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
os_type (str | None): OS key (e.g. 'ubuntu24', 'macos').
|
||||||
|
If None, automatically detects current OS.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the OS is supported and a routine was executed, else False.
|
||||||
|
"""
|
||||||
|
os_type = os_type or get_os_type()
|
||||||
|
if not os_type:
|
||||||
|
return False
|
||||||
|
|
||||||
|
routine = SUPPORTED_ROUTINES.get(os_type)
|
||||||
|
if not routine:
|
||||||
|
return False
|
||||||
|
|
||||||
|
routine()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_install_routine()
|
||||||
116
gfd/core/sfd.py
Normal file
116
gfd/core/sfd.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
"""
|
||||||
|
sfd.py - Installer List Parser for Soporte Firma Digital
|
||||||
|
|
||||||
|
This module is responsible for fetching and parsing the list of available
|
||||||
|
digital signature installers from the official Soporte Firma Digital website.
|
||||||
|
|
||||||
|
It retrieves both the visible installer names (from the page’s <select> element)
|
||||||
|
and the associated MD5 hashes (embedded within inline JavaScript).
|
||||||
|
|
||||||
|
The parsed results are used by the UI layer to display which installers
|
||||||
|
are available and to match system OS types to the appropriate installer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
SFD_URL = "https://soportefirmadigital.com/sfdj/dl.aspx"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_text(text: str) -> str:
|
||||||
|
"""
|
||||||
|
Normalize text for consistent matching.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Remove accents and diacritics using Unicode normalization.
|
||||||
|
2. Replace consecutive spaces with a single space.
|
||||||
|
3. Convert to lowercase and strip leading/trailing whitespace.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): Raw string to normalize.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Cleaned, normalized string suitable for fuzzy matching.
|
||||||
|
"""
|
||||||
|
text = unicodedata.normalize("NFKD", text)
|
||||||
|
text = "".join(c for c in text if not unicodedata.combining(c))
|
||||||
|
return re.sub(r"\s+", " ", text).strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def fetchInstallerOptions(url: str = SFD_URL):
|
||||||
|
"""
|
||||||
|
Fetch and parse the installer list from the Soporte Firma Digital website.
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
[
|
||||||
|
{"name": "Usuarios Linux - Ubuntu 24.04 LTS (DEB 64bits) - 78 MB",
|
||||||
|
"md5": "bdc871e15f2096f930b285f0ed799aa0"},
|
||||||
|
{"name": "Usuarios Windows JCOP3 - 189 MB",
|
||||||
|
"md5": "596c347e409d00388c3c1ee0a72b96c0"},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str, optional): Target URL to fetch. Defaults to SFD_URL.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict]: Each dict contains:
|
||||||
|
- "name" (str): Installer display name.
|
||||||
|
- "md5" (str | None): MD5 checksum (if found), else None.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
r = requests.get(url, timeout=8)
|
||||||
|
r.raise_for_status()
|
||||||
|
except requests.RequestException:
|
||||||
|
# Network error, timeout, or unreachable server
|
||||||
|
return []
|
||||||
|
|
||||||
|
soup = BeautifulSoup(r.text, "html.parser")
|
||||||
|
|
||||||
|
# Extract installer names from the dropdown list
|
||||||
|
installers = [
|
||||||
|
opt.text.strip()
|
||||||
|
for opt in soup.select("#ctl00_certContents_ddlInstaladores option")
|
||||||
|
]
|
||||||
|
if not installers:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Parse embedded JavaScript to extract name, MD5 mapping
|
||||||
|
md5s = {}
|
||||||
|
for name, md5 in re.findall(
|
||||||
|
r"text\s*==\s*'([^']+)'.*?MD5\s*[:=]\s*([A-Za-z0-9]+)",
|
||||||
|
r.text,
|
||||||
|
re.DOTALL,
|
||||||
|
):
|
||||||
|
md5 = md5.lower()
|
||||||
|
if re.fullmatch(r"[0-9a-f]{32}", md5): # Only valid 32-character MD5
|
||||||
|
md5s[_normalize_text(name)] = md5
|
||||||
|
|
||||||
|
# Merge name list with MD5 map
|
||||||
|
results = []
|
||||||
|
for inst in installers:
|
||||||
|
n = _normalize_text(inst)
|
||||||
|
md5 = md5s.get(n)
|
||||||
|
|
||||||
|
# Try fuzzy match if exact normalization key not found
|
||||||
|
if not md5:
|
||||||
|
for key, val in md5s.items():
|
||||||
|
if key in n or n in key:
|
||||||
|
md5 = val
|
||||||
|
break
|
||||||
|
|
||||||
|
results.append({"name": inst, "md5": md5})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
"""
|
||||||
|
Fetch and print all installers to stdout.
|
||||||
|
"""
|
||||||
|
installers = fetchInstallerOptions()
|
||||||
|
print(f"Fetched {len(installers)} installers from {SFD_URL}\n")
|
||||||
|
for item in installers:
|
||||||
|
print(f"{item['name']:<70} MD5={item['md5'] or 'N/A'}")
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
translations = {
|
translations = {
|
||||||
"hello_world": "Hello, World!",
|
|
||||||
"window_title": "Digital Signature Manager",
|
"window_title": "Digital Signature Manager",
|
||||||
"no_installers_found": "Could not update installer list",
|
"no_install_message": "Digital signature not installed",
|
||||||
"loading_installers": "Loading Installers",
|
"recommended_version": "Recommended version:",
|
||||||
"refresh": "Refresh",
|
"install": "Install",
|
||||||
|
"installed_version": "Installed version:",
|
||||||
|
"latest_version_installed": "Latest version installed",
|
||||||
|
"update_digital_signature": "Update Digital Signature",
|
||||||
|
"no_installers_found": "Could not fetch the installer list",
|
||||||
|
"loading_installers": "Loading installers",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
translations = {
|
translations = {
|
||||||
"hello_world": "¡Hola, Mundo!",
|
|
||||||
"window_title": "Gestor de Firma Digital",
|
"window_title": "Gestor de Firma Digital",
|
||||||
"no_installers_found": "No se pudo actualizar la lista de instaladores",
|
"no_install_message": "No tiene la firma digital instalada",
|
||||||
|
"recommended_version": "Versión recomendada:",
|
||||||
|
"install": "Instalar",
|
||||||
|
"installed_version": "Versión instalada:",
|
||||||
|
"latest_version_installed": "Última versión instalada",
|
||||||
|
"update_digital_signature": "Actualizar Firma Digital",
|
||||||
|
"no_installers_found": "No se pudo obtener la lista de instaladores", # ES
|
||||||
"loading_installers": "Cargando instaladores",
|
"loading_installers": "Cargando instaladores",
|
||||||
"refresh": "Buscar",
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
68
gfd/sfd.py
68
gfd/sfd.py
|
|
@ -1,68 +0,0 @@
|
||||||
import re
|
|
||||||
import unicodedata
|
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
SFD_URL = "https://soportefirmadigital.com/sfdj/dl.aspx"
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_text(text: str) -> str:
|
|
||||||
"""Internal: remove accents, extra spaces, and lowercase."""
|
|
||||||
text = unicodedata.normalize("NFKD", text)
|
|
||||||
text = "".join(c for c in text if not unicodedata.combining(c))
|
|
||||||
return re.sub(r"\s+", " ", text).strip().lower()
|
|
||||||
|
|
||||||
|
|
||||||
def fetchInstallerOptions(url: str = SFD_URL):
|
|
||||||
"""
|
|
||||||
Fetch the list of installer options from the given Soporte Firma Digital page.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url (str, optional): Page URL to fetch. Defaults to SFD_URL.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[dict]: Each item is {'name': <str>, 'md5': <str or None>}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
r = requests.get(url, timeout=8)
|
|
||||||
r.raise_for_status()
|
|
||||||
except requests.RequestException:
|
|
||||||
return []
|
|
||||||
|
|
||||||
soup = BeautifulSoup(r.text, "html.parser")
|
|
||||||
|
|
||||||
installers = [
|
|
||||||
opt.text.strip()
|
|
||||||
for opt in soup.select("#ctl00_certContents_ddlInstaladores option")
|
|
||||||
]
|
|
||||||
if not installers:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Extract valid MD5 hashes from embedded JavaScript
|
|
||||||
md5s = {}
|
|
||||||
for name, md5 in re.findall(
|
|
||||||
r"text\s*==\s*'([^']+)'.*?MD5\s*[:=]\s*([A-Za-z0-9]+)",
|
|
||||||
r.text,
|
|
||||||
re.DOTALL,
|
|
||||||
):
|
|
||||||
md5 = md5.lower()
|
|
||||||
if re.fullmatch(r"[0-9a-f]{32}", md5): # only valid 32-char hex hashes
|
|
||||||
md5s[_normalize_text(name)] = md5
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for inst in installers:
|
|
||||||
n = _normalize_text(inst)
|
|
||||||
md5 = md5s.get(n)
|
|
||||||
if not md5:
|
|
||||||
for key, val in md5s.items():
|
|
||||||
if key in n or n in key:
|
|
||||||
md5 = val
|
|
||||||
break
|
|
||||||
results.append({"name": inst, "md5": md5})
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
for item in fetchInstallerOptions():
|
|
||||||
print(f"{item['name']:<70} MD5={item['md5'] or 'N/A'}")
|
|
||||||
222
gfd/ui/main_window.py
Normal file
222
gfd/ui/main_window.py
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
from PySide6 import QtWidgets, QtCore
|
||||||
|
from gfd.core.osinfo import get_os_type
|
||||||
|
from gfd.core.installers import get_available_installers, get_installed_version
|
||||||
|
|
||||||
|
|
||||||
|
def check_installation_status():
|
||||||
|
"""
|
||||||
|
Determine system support and installer availability.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(installed: [version, md5] or [],
|
||||||
|
available: list[[version, md5]],
|
||||||
|
recommended: [version, md5] or None)
|
||||||
|
"""
|
||||||
|
os_type = get_os_type()
|
||||||
|
|
||||||
|
if not os_type:
|
||||||
|
return [], [], None
|
||||||
|
|
||||||
|
available = get_available_installers(os_type)
|
||||||
|
installed = get_installed_version()
|
||||||
|
|
||||||
|
if not available:
|
||||||
|
return installed, [], None
|
||||||
|
|
||||||
|
# Assume the first installer in the list is the recommended one
|
||||||
|
recommended = available[0]
|
||||||
|
|
||||||
|
return installed, available, recommended
|
||||||
|
|
||||||
|
|
||||||
|
class GFDWidget(QtWidgets.QWidget):
|
||||||
|
def __init__(self, tr):
|
||||||
|
super().__init__()
|
||||||
|
self.tr = tr
|
||||||
|
self.setWindowTitle(self.tr("window_title"))
|
||||||
|
self.resize(600, 400)
|
||||||
|
|
||||||
|
self.main_layout = QtWidgets.QVBoxLayout(self)
|
||||||
|
self.main_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
|
||||||
|
# Card container
|
||||||
|
self.card = QtWidgets.QFrame()
|
||||||
|
self.card_layout = QtWidgets.QVBoxLayout(self.card)
|
||||||
|
self.card_layout.setSpacing(12)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
self.title = QtWidgets.QLabel(self.tr("window_title"))
|
||||||
|
self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.title.setStyleSheet("font-weight: bold; font-size: 16px;")
|
||||||
|
self.card_layout.addWidget(self.title)
|
||||||
|
|
||||||
|
# Content area
|
||||||
|
self.content_area = QtWidgets.QWidget()
|
||||||
|
self.content_layout = QtWidgets.QVBoxLayout(self.content_area)
|
||||||
|
self.content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.card_layout.addWidget(self.content_area)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
self.footer = QtWidgets.QHBoxLayout()
|
||||||
|
self.footer.addStretch()
|
||||||
|
self.refresh_btn = QtWidgets.QPushButton("⟳")
|
||||||
|
self.refresh_btn.setFixedWidth(36)
|
||||||
|
self.refresh_btn.clicked.connect(self.refresh_state)
|
||||||
|
self.footer.addWidget(self.refresh_btn)
|
||||||
|
self.card_layout.addLayout(self.footer)
|
||||||
|
|
||||||
|
self.main_layout.addWidget(self.card)
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton[primary="true"] {
|
||||||
|
background-color: #2f80ed;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
QPushButton[primary="true"]:disabled {
|
||||||
|
background-color: #a0c4ff;
|
||||||
|
}
|
||||||
|
QLabel, QComboBox {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.states = ["not_installed", "installed", "update_available"]
|
||||||
|
self.current_state = 0
|
||||||
|
|
||||||
|
# Run initial refresh
|
||||||
|
self.refresh_state()
|
||||||
|
|
||||||
|
# --- LOGIC ---
|
||||||
|
|
||||||
|
def refresh_state(self):
|
||||||
|
"""Fetch installer list and determine status."""
|
||||||
|
self.refresh_btn.setEnabled(False)
|
||||||
|
self.clear_content()
|
||||||
|
loading_label = QtWidgets.QLabel(self.tr("loading_installers") + "…")
|
||||||
|
loading_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.content_layout.addWidget(loading_label)
|
||||||
|
|
||||||
|
QtCore.QTimer.singleShot(150, self._refresh_check)
|
||||||
|
|
||||||
|
def _refresh_check(self):
|
||||||
|
"""Evaluate OS type, installed version, and available installers."""
|
||||||
|
self.installed, self.available, self.recommended = check_installation_status()
|
||||||
|
|
||||||
|
print("\n=== INSTALLER STATUS DEBUG ===")
|
||||||
|
if not self.available:
|
||||||
|
print("No available installers for this OS.")
|
||||||
|
else:
|
||||||
|
for i, (name, md5) in enumerate(self.available, start=1):
|
||||||
|
print(f"{i:2d}. {name:<70} MD5={md5 or 'N/A'}")
|
||||||
|
print("==============================\n")
|
||||||
|
|
||||||
|
# 🧩 Print simulated installed state
|
||||||
|
if not self.installed:
|
||||||
|
print("→ Status: NOT INSTALLED")
|
||||||
|
else:
|
||||||
|
print(f"→ Installed: {self.installed[0]} MD5={self.installed[1]}")
|
||||||
|
|
||||||
|
if not self.available:
|
||||||
|
self.show_error()
|
||||||
|
elif not self.recommended:
|
||||||
|
print("→ Decision: OS NOT SUPPORTED")
|
||||||
|
self.show_error()
|
||||||
|
elif not self.installed:
|
||||||
|
print(f"→ Decision: NOT INSTALLED (Recommended: {self.recommended[0]})")
|
||||||
|
self.set_state("not_installed")
|
||||||
|
else:
|
||||||
|
installed_tuple = tuple(self.installed)
|
||||||
|
available_tuples = [tuple(a) for a in self.available]
|
||||||
|
|
||||||
|
if installed_tuple in available_tuples:
|
||||||
|
print("→ Decision: INSTALLED (latest version matches)")
|
||||||
|
self.set_state("installed")
|
||||||
|
else:
|
||||||
|
print("→ Decision: UPDATE AVAILABLE (installed not in available list)")
|
||||||
|
self.set_state("update_available")
|
||||||
|
|
||||||
|
self.refresh_btn.setEnabled(True)
|
||||||
|
|
||||||
|
def clear_content(self):
|
||||||
|
"""Remove all widgets from content area."""
|
||||||
|
for i in reversed(range(self.content_layout.count())):
|
||||||
|
widget = self.content_layout.itemAt(i).widget()
|
||||||
|
if widget:
|
||||||
|
widget.setParent(None)
|
||||||
|
|
||||||
|
# --- UI STATES ---
|
||||||
|
|
||||||
|
def update_state(self):
|
||||||
|
"""Render the current UI state."""
|
||||||
|
self.clear_content()
|
||||||
|
state = self.states[self.current_state]
|
||||||
|
|
||||||
|
if state == "not_installed":
|
||||||
|
self.show_not_installed()
|
||||||
|
elif state == "installed":
|
||||||
|
self.show_installed()
|
||||||
|
elif state == "update_available":
|
||||||
|
self.show_update_available()
|
||||||
|
|
||||||
|
def show_error(self):
|
||||||
|
"""Shown when no installer data is available."""
|
||||||
|
self.clear_content()
|
||||||
|
lbl = QtWidgets.QLabel(self.tr("no_installers_found"))
|
||||||
|
lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.content_layout.addWidget(lbl)
|
||||||
|
|
||||||
|
def show_not_installed(self):
|
||||||
|
recommended_name = self.recommended[0] if self.recommended else "?"
|
||||||
|
lbl = QtWidgets.QLabel(
|
||||||
|
f"{self.tr('no_install_message')}<br>"
|
||||||
|
f"{self.tr('recommended_version')}<br><b>{recommended_name}</b>"
|
||||||
|
)
|
||||||
|
lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.content_layout.addWidget(lbl)
|
||||||
|
|
||||||
|
btn = QtWidgets.QPushButton(self.tr("install"))
|
||||||
|
btn.setProperty("primary", True)
|
||||||
|
self.content_layout.addWidget(btn)
|
||||||
|
|
||||||
|
combo = QtWidgets.QComboBox()
|
||||||
|
for version, md5 in self.available:
|
||||||
|
combo.addItem(f"{version} (MD5: {md5 or 'N/A'})")
|
||||||
|
self.content_layout.addWidget(combo)
|
||||||
|
|
||||||
|
def show_installed(self):
|
||||||
|
version, md5 = self.installed
|
||||||
|
lbl = QtWidgets.QLabel(
|
||||||
|
f"{self.tr('installed_version')}<br><b>{version}</b><br>MD5: {md5 or 'N/A'}"
|
||||||
|
)
|
||||||
|
lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.content_layout.addWidget(lbl)
|
||||||
|
|
||||||
|
btn = QtWidgets.QPushButton(self.tr("latest_version_installed"))
|
||||||
|
btn.setEnabled(False)
|
||||||
|
self.content_layout.addWidget(btn)
|
||||||
|
|
||||||
|
def show_update_available(self):
|
||||||
|
version, md5 = self.installed
|
||||||
|
lbl = QtWidgets.QLabel(
|
||||||
|
f"{self.tr('installed_version')}<br><b>{version}</b><br>MD5: {md5 or 'N/A'}"
|
||||||
|
)
|
||||||
|
lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.content_layout.addWidget(lbl)
|
||||||
|
|
||||||
|
btn = QtWidgets.QPushButton(self.tr("update_digital_signature"))
|
||||||
|
btn.setProperty("primary", True)
|
||||||
|
self.content_layout.addWidget(btn)
|
||||||
|
|
||||||
|
combo = QtWidgets.QComboBox()
|
||||||
|
for version, md5 in self.available:
|
||||||
|
combo.addItem(f"{version} (MD5: {md5 or 'N/A'})")
|
||||||
|
self.content_layout.addWidget(combo)
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
if state in self.states:
|
||||||
|
self.current_state = self.states.index(state)
|
||||||
|
self.update_state()
|
||||||
Loading…
Reference in a new issue