2420091a4e
The update check asked PyPI for the `ytsage` package's version, asked GitHub for oop7/YTSage's release notes, and pointed the download button at upstream's releases. None of that describes this program. Being reminded to install YTSage was the visible symptom; the cause was that the check had never been repointed when the fork was made. It now reads SageTube's own releases from git.houmeres.sk. Gitea's release API is shaped like GitHub's, so the dialog and the caller are unchanged -- the class keeps its name and signal signature, and only its body moved out to core/ytsage_app_update.py, which is fork-owned and will not conflict on the next merge from upstream. Upstream's inherited tags end in `b` (v5.3.0b and earlier). packaging reads that as a beta marker, so they sort below v5.4.0 and a stable instance cannot be handed one. Tag parsing is defensive anyway: one unparseable tag must not take the whole check down with it. Also: the check is rate-limited to once a day rather than every start, the dialog gained a "Skip this version" that survives a restart, the thread is now joined on close, and the About dialog says SageTube. The three binary updaters -- yt-dlp, Deno, ffmpeg -- legitimately track their own upstreams and are deliberately untouched; ytsage_app_update's docstring says so, because "update" is an overloaded word in this codebase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
644 lines
23 KiB
Python
644 lines
23 KiB
Python
"""
|
|
Base dialogs for YTSage application.
|
|
Contains basic utility dialogs like LogWindow and AboutDialog.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from PySide6.QtCore import Qt, QThread, QTimer, Signal, QUrl
|
|
from PySide6.QtGui import QDesktopServices
|
|
from PySide6.QtWidgets import (
|
|
QDialog,
|
|
QDialogButtonBox,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QMessageBox,
|
|
QPushButton,
|
|
QSizePolicy,
|
|
QTextEdit,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from ... import __version__ as APP_VERSION
|
|
from ...utils.ytsage_localization import _
|
|
from ...utils.ytsage_logger import logger
|
|
from ...utils.ytsage_constants import APP_LOG_DIR
|
|
|
|
from ...core.ytsage_ffmpeg import get_ffmpeg_path
|
|
from ...core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, get_deno_version, refresh_version_cache
|
|
from ...core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path, check_ytdlp_deno_integration
|
|
from ...core.ytsage_deno import check_deno_installed, get_deno_path
|
|
|
|
|
|
class SystemInfoThread(QThread):
|
|
"""Background thread to gather system information."""
|
|
info_ready = Signal(dict)
|
|
|
|
def run(self):
|
|
info = {}
|
|
|
|
# yt-dlp Status
|
|
ytdlp_found = check_ytdlp_installed()
|
|
info['ytdlp_found'] = ytdlp_found
|
|
info['ytdlp_version'] = get_ytdlp_version()
|
|
info['ytdlp_path'] = get_yt_dlp_path() if ytdlp_found else None
|
|
|
|
# yt-dlp cache status
|
|
ytdlp_cache = _version_cache.get("ytdlp", {})
|
|
info['ytdlp_last_check'] = ytdlp_cache.get("last_check", 0)
|
|
|
|
# FFmpeg Status
|
|
ffmpeg_found = check_ffmpeg()
|
|
info['ffmpeg_found'] = ffmpeg_found
|
|
info['ffmpeg_version'] = get_ffmpeg_version() if ffmpeg_found else _('about.not_available')
|
|
info['ffmpeg_path'] = get_ffmpeg_path() if ffmpeg_found else None
|
|
|
|
# FFmpeg cache status
|
|
ffmpeg_cache = _version_cache.get("ffmpeg", {})
|
|
info['ffmpeg_last_check'] = ffmpeg_cache.get("last_check", 0)
|
|
|
|
# Deno Status
|
|
deno_found = check_deno_installed()
|
|
info['deno_found'] = deno_found
|
|
info['deno_version'] = get_deno_version() if deno_found else _('about.not_available')
|
|
info['deno_path'] = get_deno_path() if deno_found else None
|
|
|
|
# Deno cache status
|
|
deno_cache = _version_cache.get("deno", {})
|
|
info['deno_last_check'] = deno_cache.get("last_check", 0)
|
|
|
|
# Check integration with yt-dlp if both are present
|
|
info['integration_status'] = False
|
|
if deno_found and ytdlp_found:
|
|
info['integration_status'] = check_ytdlp_deno_integration()
|
|
|
|
self.info_ready.emit(info)
|
|
|
|
|
|
class LogWindow(QDialog):
|
|
def __init__(self, parent=None) -> None:
|
|
super().__init__(parent)
|
|
self.setWindowTitle(_("dialogs.ytdlp_log_title"))
|
|
self.setMinimumSize(700, 500)
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
self.log_text = QTextEdit()
|
|
self.log_text.setReadOnly(True)
|
|
self.log_text.setStyleSheet(
|
|
"""
|
|
QTextEdit {
|
|
background-color: #2b2b2b;
|
|
color: #ffffff;
|
|
font-family: Consolas, monospace;
|
|
font-size: 12px;
|
|
border: 2px solid #3d3d3d;
|
|
border-radius: 4px;
|
|
}
|
|
"""
|
|
)
|
|
|
|
layout.addWidget(self.log_text)
|
|
|
|
def append_log(self, message) -> None:
|
|
self.log_text.append(message)
|
|
# Auto-scroll to bottom
|
|
scrollbar = self.log_text.verticalScrollBar()
|
|
scrollbar.setValue(scrollbar.maximum())
|
|
|
|
|
|
class AboutDialog(QDialog):
|
|
def __init__(self, parent=None) -> None:
|
|
super().__init__(parent)
|
|
self._parent = parent # Store parent to access version etc.
|
|
self.setWindowTitle(_("about.title"))
|
|
self.setMinimumSize(460, 420) # Slightly increased to accommodate paths
|
|
self.resize(460, 440) # Slightly increased initial size
|
|
self.setMaximumSize(500, 480) # Reasonable maximum size
|
|
|
|
# Set window flags to make dialog independent of parent movement
|
|
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowCloseButtonHint)
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(15) # Reduced spacing
|
|
layout.setContentsMargins(20, 20, 20, 20) # Reduced margins
|
|
layout.setSizeConstraint(QVBoxLayout.SizeConstraint.SetMinAndMaxSize)
|
|
|
|
# App Information Section
|
|
app_info_widget = self._create_app_info_section()
|
|
layout.addWidget(app_info_widget)
|
|
|
|
# Separator - subtle and compact
|
|
separator = QWidget()
|
|
separator.setFixedHeight(1)
|
|
separator.setStyleSheet("background-color: #2a2a2a; margin: 8px 20px;")
|
|
layout.addWidget(separator)
|
|
|
|
# System Information Section
|
|
system_info_widget = self._create_system_info_section()
|
|
layout.addWidget(system_info_widget)
|
|
|
|
# Close Button - compact positioning
|
|
layout.addSpacing(10) # Reduced space before button
|
|
button_layout = QHBoxLayout()
|
|
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
|
button_box.accepted.connect(self.accept)
|
|
|
|
# Center the button
|
|
button_layout.addStretch()
|
|
button_layout.addWidget(button_box)
|
|
button_layout.addStretch()
|
|
layout.addLayout(button_layout)
|
|
|
|
# Apply overall styling - improved consistency
|
|
self.setStyleSheet(
|
|
"""
|
|
QDialog {
|
|
background-color: #15181b;
|
|
color: #ffffff;
|
|
}
|
|
QLabel {
|
|
color: #cccccc;
|
|
}
|
|
QPushButton {
|
|
padding: 10px 30px;
|
|
background-color: #c90000;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 12px;
|
|
min-width: 80px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #a50000;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #800000;
|
|
}
|
|
QGroupBox {
|
|
font-weight: bold;
|
|
border: 1px solid #333333;
|
|
border-radius: 8px;
|
|
margin-top: 12px;
|
|
padding-top: 12px;
|
|
background-color: #15181b;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
left: 15px;
|
|
padding: 0 10px 0 10px;
|
|
color: #ffffff;
|
|
font-size: 14px;
|
|
}
|
|
"""
|
|
)
|
|
|
|
def _create_app_info_section(self) -> QWidget:
|
|
"""Create the application information section - compact version"""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
layout.setSpacing(6) # Reduced spacing
|
|
|
|
# Title and Version - more compact
|
|
title_label = QLabel(
|
|
"<span style='color: #c90000; font-size: 28px; font-weight: 300; letter-spacing: 2px;'>SageTube</span>"
|
|
)
|
|
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
layout.addWidget(title_label)
|
|
|
|
version_label = QLabel(
|
|
f"<span style='color: #cccccc; font-size: 13px; font-weight: normal;'>"
|
|
f"{_('about.version', version=getattr(self._parent, 'version', APP_VERSION))}</span>"
|
|
)
|
|
version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
layout.addWidget(version_label)
|
|
|
|
# Description - more compact
|
|
description_label = QLabel(_("about.description"))
|
|
description_label.setWordWrap(True)
|
|
description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
description_label.setStyleSheet("color: #ffffff; font-size: 11px; margin: 6px 0;")
|
|
layout.addWidget(description_label)
|
|
|
|
# Author and Links - compact single line
|
|
info_layout = QHBoxLayout()
|
|
info_layout.setSpacing(15)
|
|
|
|
author_link = '<a href="https://git.houmeres.sk/Houmeres" style="color: #c90000; text-decoration: none;">Houmeres</a>'
|
|
author_label = QLabel(
|
|
f"{_('about.author', author=author_link)}"
|
|
)
|
|
author_label.setOpenExternalLinks(True)
|
|
info_layout.addWidget(author_label)
|
|
|
|
repo_link = '<a href="https://git.houmeres.sk/Houmeres/SageTube" style="color: #c90000; text-decoration: none;">SageTube</a>'
|
|
repo_label = QLabel(
|
|
f"{_('about.github', repo=repo_link)}"
|
|
)
|
|
repo_label.setOpenExternalLinks(True)
|
|
info_layout.addWidget(repo_label)
|
|
|
|
upstream_link = (
|
|
'<a href="https://github.com/oop7/YTSage" style="color: #c90000; text-decoration: none;">'
|
|
"Based on YTSage by oop7</a>"
|
|
)
|
|
upstream_label = QLabel(upstream_link)
|
|
upstream_label.setOpenExternalLinks(True)
|
|
info_layout.addWidget(upstream_label)
|
|
|
|
# Center the info layout
|
|
info_container = QHBoxLayout()
|
|
info_container.addStretch()
|
|
info_container.addLayout(info_layout)
|
|
info_container.addStretch()
|
|
|
|
layout.addLayout(info_container)
|
|
|
|
return widget
|
|
|
|
def _create_system_info_section(self) -> QWidget:
|
|
"""Create the system information section with compact design"""
|
|
# Create main container with compact styling
|
|
container = QWidget()
|
|
container.setStyleSheet(
|
|
"""
|
|
QWidget {
|
|
border: 1px solid #333333;
|
|
border-radius: 8px;
|
|
background-color: #15181b;
|
|
margin-top: 5px;
|
|
}
|
|
"""
|
|
)
|
|
|
|
main_layout = QVBoxLayout(container)
|
|
main_layout.setSpacing(8) # Compact spacing
|
|
main_layout.setContentsMargins(15, 10, 15, 10)
|
|
|
|
# Create header with title and refresh button on same line
|
|
header_layout = QHBoxLayout()
|
|
header_layout.setContentsMargins(0, 0, 0, 5)
|
|
|
|
# System Information title
|
|
title_label = QLabel(_("about.system_info"))
|
|
title_label.setStyleSheet(
|
|
"""
|
|
QLabel {
|
|
color: #ffffff;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
padding: 0px;
|
|
margin: 0px;
|
|
}
|
|
"""
|
|
)
|
|
header_layout.addWidget(title_label)
|
|
|
|
# Add stretch to push refresh button to the right
|
|
header_layout.addStretch()
|
|
|
|
# Create logs button (minimal)
|
|
self.logs_btn = QPushButton(_("about.open_logs")) # Expected to be small text or icon
|
|
self.logs_btn.setToolTip(_("about.logs_tooltip"))
|
|
self.logs_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
self.logs_btn.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 1px 6px;
|
|
background-color: transparent;
|
|
border: 1px solid #333;
|
|
border-radius: 4px;
|
|
color: #888888;
|
|
font-size: 10px;
|
|
margin-right: 8px;
|
|
}
|
|
QPushButton:hover {
|
|
color: #ffffff;
|
|
border-color: #555;
|
|
background-color: rgba(255, 255, 255, 0.05);
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
"""
|
|
)
|
|
self.logs_btn.clicked.connect(self.open_logs_folder)
|
|
header_layout.addWidget(self.logs_btn)
|
|
|
|
# Create refresh button
|
|
self.refresh_btn = QPushButton(_("about.refresh"))
|
|
self.refresh_btn.setFixedSize(16, 16)
|
|
self.refresh_btn.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 0px;
|
|
background-color: transparent;
|
|
border: none;
|
|
color: #cccccc;
|
|
font-size: 10px;
|
|
font-weight: normal;
|
|
margin: 0px;
|
|
}
|
|
QPushButton:hover {
|
|
color: #ffffff;
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
}
|
|
QPushButton:pressed {
|
|
color: #c90000;
|
|
background-color: rgba(201, 0, 0, 0.1);
|
|
}
|
|
"""
|
|
)
|
|
self.refresh_btn.clicked.connect(self.refresh_version_info)
|
|
header_layout.addWidget(self.refresh_btn)
|
|
|
|
main_layout.addLayout(header_layout)
|
|
|
|
# Compact status grid layout
|
|
self.status_container = QVBoxLayout()
|
|
self.status_container.setSpacing(6) # Tight spacing
|
|
self.status_container.setContentsMargins(0, 0, 0, 0)
|
|
|
|
main_layout.addLayout(self.status_container)
|
|
|
|
# Show loading message initially
|
|
self._show_loading_message()
|
|
|
|
# Populate system information asynchronously
|
|
QTimer.singleShot(100, self.update_system_info)
|
|
|
|
return container
|
|
|
|
def _show_loading_message(self) -> None:
|
|
"""Show a compact loading message while system information is being gathered."""
|
|
# Strip the emoji from the localized string to avoid rendering issues
|
|
loading_text = _("about.loading").replace("🔄", "").strip()
|
|
loading_label = QLabel(loading_text)
|
|
loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
loading_label.setStyleSheet(
|
|
"""
|
|
QLabel {
|
|
color: #888888;
|
|
font-size: 11px;
|
|
padding: 10px;
|
|
}
|
|
"""
|
|
)
|
|
|
|
self.status_container.addWidget(loading_label)
|
|
|
|
def _create_status_item(self, icon, name, status_text, version_text, path_text=None, cache_status="") -> QWidget:
|
|
"""Create a compact status item widget"""
|
|
item_widget = QWidget()
|
|
# Adjust height based on whether we have path info
|
|
item_height = 50 if path_text else 35
|
|
item_widget.setMaximumHeight(item_height)
|
|
item_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
|
|
# Main layout
|
|
item_layout = QVBoxLayout(item_widget)
|
|
item_layout.setContentsMargins(8, 4, 8, 4)
|
|
item_layout.setSpacing(2)
|
|
|
|
# First row: Icon, name, status, version
|
|
first_row = QHBoxLayout()
|
|
first_row.setSpacing(8)
|
|
|
|
# Icon and name - compact
|
|
name_label = QLabel(f"{icon} <b>{name}</b>")
|
|
name_label.setStyleSheet("font-size: 12px; color: #ffffff; font-weight: bold;")
|
|
name_label.setMinimumWidth(80)
|
|
first_row.addWidget(name_label)
|
|
|
|
# Status - compact
|
|
status_label = QLabel(status_text)
|
|
status_label.setStyleSheet("font-size: 11px; font-weight: bold;")
|
|
status_label.setMinimumWidth(70)
|
|
first_row.addWidget(status_label)
|
|
|
|
# Version info - improved readability
|
|
version_info = version_text
|
|
if cache_status:
|
|
version_info += cache_status
|
|
|
|
version_label = QLabel(version_info)
|
|
version_label.setStyleSheet("font-size: 11px; color: #cccccc;") # Increased from 10px
|
|
version_label.setWordWrap(False)
|
|
first_row.addWidget(version_label)
|
|
|
|
# Add stretch to push everything left
|
|
first_row.addStretch()
|
|
|
|
item_layout.addLayout(first_row)
|
|
|
|
# Second row: Path (if provided)
|
|
if path_text:
|
|
path_label = QLabel(f"📁 {path_text}")
|
|
path_label.setStyleSheet("font-size: 10px; color: #aaaaaa; margin-left: 12px;") # Increased from 9px, better color
|
|
path_label.setWordWrap(False)
|
|
# Truncate very long paths
|
|
if len(str(path_text)) > 60:
|
|
truncated_path = "..." + str(path_text)[-57:]
|
|
path_label.setText(f"📁 {truncated_path}")
|
|
item_layout.addWidget(path_label)
|
|
|
|
# Subtle background with minimal border
|
|
item_widget.setStyleSheet(
|
|
"""
|
|
QWidget {
|
|
background-color: rgba(45, 45, 45, 0.3);
|
|
border: 1px solid #2a2a2a;
|
|
border-radius: 4px;
|
|
margin: 1px;
|
|
}
|
|
QWidget:hover {
|
|
background-color: rgba(60, 60, 60, 0.4);
|
|
}
|
|
"""
|
|
)
|
|
|
|
return item_widget
|
|
|
|
def update_system_info(self) -> None:
|
|
"""Start background thread to gather system info."""
|
|
self.info_thread = SystemInfoThread()
|
|
self.info_thread.info_ready.connect(self._populate_system_info)
|
|
self.info_thread.start()
|
|
|
|
def _populate_system_info(self, info: dict) -> None:
|
|
"""Populate UI with gathered info."""
|
|
# Clear existing items
|
|
for i in reversed(range(self.status_container.count())):
|
|
child = self.status_container.itemAt(i).widget()
|
|
if child:
|
|
child.deleteLater()
|
|
|
|
# yt-dlp Status - compact version with path
|
|
ytdlp_found = info['ytdlp_found']
|
|
ytdlp_status_text = (
|
|
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>" if ytdlp_found else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
|
|
)
|
|
ytdlp_version = info['ytdlp_version']
|
|
|
|
# Get yt-dlp path
|
|
ytdlp_path = info['ytdlp_path']
|
|
ytdlp_path_text = ytdlp_path if ytdlp_path and ytdlp_path != "yt-dlp" else None
|
|
|
|
# Simplified cache status
|
|
last_check = info['ytdlp_last_check']
|
|
cache_status = ""
|
|
if last_check > 0:
|
|
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
|
|
cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" # Increased from 9px
|
|
|
|
ytdlp_item = self._create_status_item(
|
|
"🎥",
|
|
"yt-dlp",
|
|
ytdlp_status_text,
|
|
ytdlp_version + cache_status,
|
|
ytdlp_path_text,
|
|
)
|
|
self.status_container.addWidget(ytdlp_item)
|
|
|
|
# FFmpeg Status - compact version with path
|
|
ffmpeg_found = info['ffmpeg_found']
|
|
ffmpeg_status_text = (
|
|
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>"
|
|
if ffmpeg_found
|
|
else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
|
|
)
|
|
ffmpeg_version = info['ffmpeg_version']
|
|
|
|
# Get FFmpeg path
|
|
ffmpeg_path_text = info['ffmpeg_path']
|
|
|
|
# Simplified cache status for FFmpeg
|
|
last_check = info['ffmpeg_last_check']
|
|
cache_status = ""
|
|
if last_check > 0 and ffmpeg_found:
|
|
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
|
|
cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" # Increased from 9px
|
|
|
|
ffmpeg_item = self._create_status_item(
|
|
"🎬",
|
|
"FFmpeg",
|
|
ffmpeg_status_text,
|
|
ffmpeg_version + cache_status,
|
|
ffmpeg_path_text,
|
|
)
|
|
self.status_container.addWidget(ffmpeg_item)
|
|
|
|
# Deno Status - compact version with path (only show path if in app bin directory)
|
|
deno_found = info['deno_found']
|
|
deno_status_text = (
|
|
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>" if deno_found else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
|
|
)
|
|
deno_version = info['deno_version']
|
|
|
|
# Get Deno path - only show if in app bin directory
|
|
deno_path_text = None
|
|
if deno_found:
|
|
deno_path = info['deno_path']
|
|
# Only show path if it's not the fallback "deno" and the file exists
|
|
if deno_path and deno_path != "deno":
|
|
from pathlib import Path
|
|
from ...utils.ytsage_constants import DENO_APP_BIN_PATH
|
|
# Check if the path is our managed binary
|
|
if Path(deno_path).resolve() == DENO_APP_BIN_PATH.resolve():
|
|
deno_path_text = deno_path
|
|
|
|
# Simplified cache status for Deno
|
|
last_check = info['deno_last_check']
|
|
cache_status = ""
|
|
if last_check > 0 and deno_found:
|
|
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
|
|
cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>"
|
|
|
|
# Check integration with yt-dlp if both are present
|
|
integration_status = ""
|
|
if info.get('integration_status', False):
|
|
integration_status = f" <span style='color: #4CAF50; font-size: 10px; font-weight: bold;'> + yt-dlp</span>"
|
|
|
|
deno_item = self._create_status_item(
|
|
"🦕",
|
|
"Deno",
|
|
deno_status_text,
|
|
deno_version + cache_status + integration_status,
|
|
deno_path_text,
|
|
)
|
|
self.status_container.addWidget(deno_item)
|
|
|
|
|
|
def open_logs_folder(self):
|
|
"""Open the application logs folder in the system file explorer."""
|
|
try:
|
|
if not APP_LOG_DIR.exists():
|
|
logger.warning(f"Log directory does not exist: {APP_LOG_DIR}")
|
|
APP_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
log_url = QUrl.fromLocalFile(str(APP_LOG_DIR))
|
|
QDesktopServices.openUrl(log_url)
|
|
except Exception as e:
|
|
logger.error(f"Failed to open log folder: {e}")
|
|
# Minimal error feedback since this is about dialog
|
|
self.logs_btn.setToolTip(f"Error: {str(e)}")
|
|
|
|
def refresh_version_info(self) -> None:
|
|
"""Refresh version information manually."""
|
|
self.refresh_btn.setText(_('about.refreshing'))
|
|
self.refresh_btn.setEnabled(False)
|
|
|
|
# Perform refresh in a separate thread to avoid blocking UI
|
|
class RefreshThread(QThread):
|
|
finished = Signal(bool)
|
|
|
|
def run(self):
|
|
success = refresh_version_cache(force=True)
|
|
self.finished.emit(success)
|
|
|
|
self.refresh_thread = RefreshThread()
|
|
self.refresh_thread.finished.connect(self.on_refresh_finished)
|
|
self.refresh_thread.start()
|
|
|
|
def on_refresh_finished(self, success) -> None:
|
|
"""Handle refresh completion."""
|
|
self.refresh_btn.setText(_('about.refresh'))
|
|
self.refresh_btn.setEnabled(True)
|
|
|
|
if success:
|
|
self.update_system_info()
|
|
else:
|
|
# Show error message with proper styling
|
|
msg_box = QMessageBox(self)
|
|
msg_box.setIcon(QMessageBox.Icon.Warning)
|
|
msg_box.setWindowTitle(_('about.refresh_failed'))
|
|
msg_box.setText(_('about.refresh_failed_message'))
|
|
msg_box.setWindowIcon(self.windowIcon())
|
|
msg_box.setStyleSheet(
|
|
"""
|
|
QMessageBox {
|
|
background-color: #15181b;
|
|
color: #ffffff;
|
|
}
|
|
QMessageBox QLabel {
|
|
color: #ffffff;
|
|
}
|
|
QMessageBox QPushButton {
|
|
padding: 8px 15px;
|
|
background-color: #c90000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
min-width: 80px;
|
|
}
|
|
QMessageBox QPushButton:hover {
|
|
background-color: #a50000;
|
|
}
|
|
"""
|
|
)
|
|
msg_box.exec()
|