diff --git a/README.md b/README.md
index 11c6db75c32a0f2e041c0de6e8dceada3a80223b..83d88a2a3941e6e49a1f0874c677ad0513048030 100644
--- a/README.md
+++ b/README.md
@@ -45,10 +45,10 @@
|指标 |数据 |
|:---|:---|
-|发布版本 |15 个版本(v1.0.0 → v1.10.1) |
-|开发周期 |108 天 |
-|总更新项 |**185 项** |
-|平均每版本 |12.3 项 |
+|发布版本 |16 个版本(v1.0.0 → v1.10.2) |
+|开发周期 |115 天 |
+|总更新项 |**191 项** |
+|平均每版本 |11.9 项 |
**详细分类统计**:
@@ -60,7 +60,7 @@
|⚡ 性能提升 |**12** |缓存机制、启动优化等 |
|📝 内容调整 |**13** |文本、名称等调整 |
|⚙️ 体验优化 |**7** |交互体验改进 |
-|🏗️ 代码优化 |**5** |代码结构优化 |
+|🏗️ 代码优化 |**6** |代码结构优化 |
|🔮 逻辑优化 |**2** |算法逻辑改进 |
|🖥️ 平台支持 |**1** |Mac 版本适配 |
|📜 许可证完善 |**1** |开源合规性 |
@@ -347,10 +347,10 @@ Since the release of the first version on 2026-02-05, the project has maintained
|Metric |Data |
|:---|:---|
-|Released Versions |15 versions (v1.0.0 → v1.10.1) |
-|Development Period |108 days |
-|Total Updates |**185 items** |
-|Average per Version |12.3 items |
+|Released Versions |16 versions (v1.0.0 → v1.10.2) |
+|Development Period |115 days |
+|Total Updates |**191 items** |
+|Average per Version |11.9 items |
**Detailed Category Statistics**:
@@ -362,7 +362,7 @@ Since the release of the first version on 2026-02-05, the project has maintained
|⚡ Performance |**12** |Cache mechanism, startup optimization |
|📝 Content Adjustments |**13** |Text, naming adjustments |
|⚙️ Experience |**7** |Interaction improvements |
-|🏗️ Code Optimization |**5** |Code structure optimization |
+|🏗️ Code Optimization |**6** |Code structure optimization |
|🔮 Logic Optimization |**2** |Algorithm improvements |
|🖥️ Platform Support |**1** |Mac version adaptation |
|📜 License Compliance |**1** |Open source compliance |
diff --git a/app_log/changelog.json b/app_log/changelog.json
index d256deb77d99d2efabb72386b7ccf2dd9be65e4c..3f2a998c80a7292b1ef7964dfb2d8f685a4567f1 100644
--- a/app_log/changelog.json
+++ b/app_log/changelog.json
@@ -1,13 +1,41 @@
{
"versions": [
{
- "version": "v1.10.1",
- "date": "2026-05-24",
+ "version": "v1.10.2",
+ "date": "2026-05-31",
"notes": [
"由于工作上的原因,未来版本会放缓维护",
"有需要什么改进的地方,欢迎大家反馈问题",
"版本存在一些bug,不太适合用于生产环境,后续会慢慢更新。"
],
+ "changes": [
+ {
+ "category": "问题修复",
+ "items": [
+ "修复更换图片时未清空旧数据,导致显示上一张图片直方图的问题",
+ "修复明度分析面板隐藏采样点状态在更换图片后失效的问题"
+ ]
+ },
+ {
+ "category": "体验优化",
+ "items": [
+ "优化明度遮罩与直方图使用一致的 Gamma 校正算法,避免两者显示不一致",
+ "优化更新日志逻辑,最新正式版时隐藏预发布版本日志",
+ "更新服务新增多平台备用方案"
+ ]
+ },
+ {
+ "category": "代码重构",
+ "items": [
+ "优化更新检查模块代码"
+ ]
+ }
+ ]
+ },
+ {
+ "version": "v1.10.1",
+ "date": "2026-05-24",
+
"changes": [
{
"category": "问题修复",
@@ -601,9 +629,13 @@
"title": "内置色彩面板",
"desc": "新增内置色彩面板,集成大量开源配色方案,包含 Open Color、Nice Color Palettes、Tailwind CSS Colors 等知名配色库,支持配色随机模式、收藏到配色管理面板、预览功能"
},
+ {
+ "title": "编辑配色对话框",
+ "desc": "新增编辑配色对话框,支持编辑配色"
+ },
{
"title": "配色管理增强",
- "desc": "为色卡收藏面板添加16进制颜色值编辑功能,新增编辑配色对话框,支持添加和编辑配色"
+ "desc": "为色卡收藏面板添加16进制颜色值编辑功能
新增“添加”按钮,支持连接编辑配色对话框"
},
{
"title": "启动体验优化",
@@ -646,8 +678,7 @@
"category": "代码重构",
"items": [
"将色卡收藏面板重命名为配色管理,将配色方案面板重命名为配色生成",
- "统一配色管理导入导出JSON格式",
- "新增编辑配色对话框"
+ "统一配色管理导入导出JSON格式"
]
}
]
diff --git a/core/__init__.py b/core/__init__.py
index 233f0e5eeb8d2231872b3056b697d146577ed13a..3bdebb48ed784ad60ce2758ed93eec0b1eddbdcd 100644
--- a/core/__init__.py
+++ b/core/__init__.py
@@ -142,6 +142,12 @@ def get_svg_color_mapper():
return SVGColorMapper()
+def get_update_service():
+ """获取更新服务(延迟导入)"""
+ from .update_service import UpdateService
+ return UpdateService()
+
+
__all__ = [
# 颜色工具函数
'generate_gradient',
@@ -246,4 +252,5 @@ __all__ = [
'get_histogram_service',
'get_luminance_service',
'get_svg_color_mapper',
+ 'get_update_service',
]
diff --git a/core/luminance_service.py b/core/luminance_service.py
index bd1a9eb7349eafb7c554159d149d15128ab66691..f4cda0070906d5c910f3eb71854d233d2694238a 100644
--- a/core/luminance_service.py
+++ b/core/luminance_service.py
@@ -15,7 +15,7 @@ from PySide6.QtCore import QObject, QThread, Signal, Qt, QTimer
from PySide6.QtGui import QColor, QImage, QPainter, QPixmap
# 项目模块导入
-from .color import get_luminance, get_zone, get_zone_bounds, _rgb_to_hsv_vectorized
+from .color import get_luminance, get_zone, get_zone_bounds, calculate_luminance_from_array, _rgb_to_hsv_vectorized
from .logger import get_logger, log_performance
logger = get_logger("luminance_service")
@@ -118,11 +118,7 @@ class LuminanceCalculator(QThread):
img_array = qimage_to_numpy(self._image)
sampled = img_array[::4, ::4]
- # 向量化明度计算 (Rec. 709)
- r = sampled[:, :, 0].astype(np.float32)
- g = sampled[:, :, 1].astype(np.float32)
- b = sampled[:, :, 2].astype(np.float32)
- luminance = (0.299 * r + 0.587 * g + 0.114 * b).astype(np.uint8)
+ luminance = calculate_luminance_from_array(sampled)
# 向量化Zone统计
zone_indices = luminance // 32
@@ -315,11 +311,7 @@ class LuminanceService(QObject):
img_array = qimage_to_numpy(image)
sampled = img_array[::sample_step, ::sample_step]
- # 向量化明度计算 (Rec. 709)
- r = sampled[:, :, 0].astype(np.float32)
- g = sampled[:, :, 1].astype(np.float32)
- b = sampled[:, :, 2].astype(np.float32)
- luminance = (0.299 * r + 0.587 * g + 0.114 * b).astype(np.uint8)
+ luminance = calculate_luminance_from_array(sampled)
# 向量化Zone统计
zone_indices = luminance // 32
@@ -486,11 +478,7 @@ class LuminanceService(QObject):
# 转为NumPy数组(像素级)
img_array = qimage_to_numpy(scaled_image)
- # 向量化明度计算 (Rec. 709标准)
- r = img_array[:, :, 0].astype(np.float32)
- g = img_array[:, :, 1].astype(np.float32)
- b = img_array[:, :, 2].astype(np.float32)
- luminance = (0.299 * r + 0.587 * g + 0.114 * b).astype(np.uint8)
+ luminance = calculate_luminance_from_array(img_array)
# 获取Zone边界并生成mask
min_lum, max_lum = get_zone_bounds(f"{zone}-{zone+1}")
diff --git a/core/update_service.py b/core/update_service.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd7bc7a3dcba903e2fb946acb8249aa8b81532fb
--- /dev/null
+++ b/core/update_service.py
@@ -0,0 +1,406 @@
+from __future__ import annotations
+# 标准库导入
+import base64
+import json
+import re
+from dataclasses import dataclass
+
+# 第三方库导入
+import requests
+from PySide6.QtCore import QThread, Signal
+
+# 项目模块导入
+from utils import tr
+from core import get_app_mode, get_platform, AppMode, Platform
+
+
+@dataclass
+class UpdateInfo:
+ """更新信息"""
+ latest_version: str
+ download_url: str
+ changelog: list[dict]
+
+
+@dataclass
+class CheckResult:
+ """更新检查结果"""
+ success: bool
+ has_update: bool
+ info: UpdateInfo | None = None
+ error_message: str = ""
+ current_version: str = ""
+
+
+_PRE_RELEASE_ORDER = {"alpha": -3, "beta": -2, "rc": -1}
+
+
+def _parse_version(version_str: str) -> tuple[list[int], int, int]:
+ """解析版本号
+
+ Args:
+ version_str: 版本号字符串
+
+ Returns:
+ tuple[list[int], int, int]: (版本号数字列表, 预发布标识, 预发布版本号)
+ 预发布标识: 0=正式版, -1=RC, -2=Beta, -3=Alpha
+ 预发布版本号: Beta1/Beta2等后面的数字,默认0
+ """
+ version_str = version_str.lstrip("v").lower()
+
+ if " · " in version_str:
+ main_part, pre_part = version_str.split(" · ", 1)
+ elif " " in version_str:
+ main_part, pre_part = version_str.split(" ", 1)
+ else:
+ main_part = version_str
+ pre_part = ""
+ for keyword in _PRE_RELEASE_ORDER:
+ if keyword in version_str:
+ idx = version_str.find(keyword)
+ main_part = version_str[:idx]
+ pre_part = version_str[idx:]
+ break
+
+ parts = re.findall(r"\d+", main_part)
+ nums = [int(p) for p in parts] if parts else [0]
+
+ pre_release = 0
+ pre_release_num = 0
+ for keyword, value in _PRE_RELEASE_ORDER.items():
+ if keyword in pre_part:
+ pre_release = value
+ match = re.search(rf"{keyword}\s*(\d+)", pre_part)
+ if match:
+ pre_release_num = int(match.group(1))
+ break
+
+ return nums, pre_release, pre_release_num
+
+
+def _compare_versions(current: str, latest: str) -> int:
+ """比较版本号
+
+ Args:
+ current: 当前版本号
+ latest: 最新版本号
+
+ Returns:
+ int: 0表示版本相同,1表示当前版本更新,-1表示有新版本
+ """
+ current_parts, current_pre, current_pre_num = _parse_version(current)
+ latest_parts, latest_pre, latest_pre_num = _parse_version(latest)
+
+ max_len = max(len(current_parts), len(latest_parts))
+ current_parts.extend([0] * (max_len - len(current_parts)))
+ latest_parts.extend([0] * (max_len - len(latest_parts)))
+
+ for c, latest_part in zip(current_parts, latest_parts):
+ if c > latest_part:
+ return 1
+ elif c < latest_part:
+ return -1
+
+ if current_pre > latest_pre:
+ return 1
+ elif current_pre < latest_pre:
+ return -1
+
+ if current_pre_num > latest_pre_num:
+ return 1
+ elif current_pre_num < latest_pre_num:
+ return -1
+
+ return 0
+
+
+def _format_changelog(changelog_data: dict, current_version: str, latest_version: str) -> list[dict]:
+ """格式化更新日志
+
+ Args:
+ changelog_data: changelog.json 解析后的数据
+ current_version: 当前版本号
+ latest_version: 最新版本号
+
+ Returns:
+ list[dict]: 版本信息列表
+ """
+ versions = changelog_data.get("versions", [])
+
+ _, latest_pre, _ = _parse_version(latest_version)
+ latest_is_release = (latest_pre == 0)
+
+ versions_to_show = []
+ for version_info in versions:
+ version_str = version_info.get("version", "").lstrip("v")
+ if _compare_versions(current_version, version_str) >= 0:
+ continue
+
+ if latest_is_release:
+ _, pre_release, _ = _parse_version(version_str)
+ if pre_release != 0:
+ continue
+
+ versions_to_show.append(version_info)
+
+ return versions_to_show
+
+
+class _ReleaseSource:
+ """Release 源抽象基类"""
+
+ def fetch_release(self) -> dict:
+ """获取最新 Release 数据
+
+ Returns:
+ dict: 统一格式的 Release 数据 {"version": str, "assets": list[dict]}
+ """
+ raise NotImplementedError
+
+ def fetch_changelog(self, current_version: str, latest_version: str) -> list[dict]:
+ """获取更新日志
+
+ Args:
+ current_version: 当前版本号
+ latest_version: 最新版本号
+
+ Returns:
+ list[dict]: 版本信息列表
+ """
+ raise NotImplementedError
+
+
+class _GiteaSource(_ReleaseSource):
+ """Gitea 类平台源(Gitee / GitCode)"""
+
+ def __init__(self, release_url: str, changelog_urls: list[str]):
+ self._release_url = release_url
+ self._changelog_urls = changelog_urls
+
+ def fetch_release(self) -> dict:
+ response = requests.get(self._release_url, timeout=8)
+ if response.status_code != 200:
+ raise requests.exceptions.HTTPError(f"HTTP {response.status_code}")
+
+ data = response.json()
+ return {
+ "version": data.get("tag_name", "").lstrip("v"),
+ "assets": data.get("assets", []),
+ }
+
+ def fetch_changelog(self, current_version: str, latest_version: str) -> list[dict]:
+ for url in self._changelog_urls:
+ try:
+ response = requests.get(url, timeout=8)
+ if response.status_code != 200:
+ continue
+
+ data = response.json()
+ content = data.get("content", "")
+ json_content = base64.b64decode(content).decode("utf-8")
+ changelog_data = json.loads(json_content)
+
+ return _format_changelog(changelog_data, current_version, latest_version)
+
+ except (requests.exceptions.RequestException, json.JSONDecodeError, KeyError, base64.binascii.Error):
+ continue
+
+ return []
+
+
+class _GitHubSource(_ReleaseSource):
+ """GitHub 平台源"""
+
+ _RELEASE_URL = "https://api.github.com/repos/qingshangongzai/Color_Card/releases/latest"
+ _CHANGELOG_URLS = [
+ "https://raw.githubusercontent.com/qingshangongzai/Color_Card/main/app_log/changelog.json",
+ "https://raw.githubusercontent.com/qingshangongzai/Color_Card/main/docs/changelog.json",
+ ]
+
+ def fetch_release(self) -> dict:
+ response = requests.get(self._RELEASE_URL, timeout=8)
+ if response.status_code != 200:
+ raise requests.exceptions.HTTPError(f"HTTP {response.status_code}")
+
+ data = response.json()
+ return {
+ "version": data.get("tag_name", "").lstrip("v"),
+ "assets": data.get("assets", []),
+ }
+
+ def fetch_changelog(self, current_version: str, latest_version: str) -> list[dict]:
+ for url in self._CHANGELOG_URLS:
+ try:
+ response = requests.get(url, timeout=8)
+ if response.status_code != 200:
+ continue
+
+ changelog_data = response.json()
+ return _format_changelog(changelog_data, current_version, latest_version)
+
+ except (requests.exceptions.RequestException, json.JSONDecodeError, KeyError):
+ continue
+
+ return []
+
+
+_SOURCES: list[_ReleaseSource] = [
+ _GiteaSource(
+ "https://gitee.com/api/v5/repos/qingshangongzai/Color_Card/releases/latest",
+ [
+ "https://gitee.com/api/v5/repos/qingshangongzai/Color_Card/contents/app_log/changelog.json?ref=main",
+ "https://gitee.com/api/v5/repos/qingshangongzai/Color_Card/contents/docs/changelog.json?ref=main",
+ ],
+ ),
+ _GiteaSource(
+ "https://gitcode.com/api/v5/repos/qingshangongzai/Color_Card/releases/latest",
+ [
+ "https://gitcode.com/api/v5/repos/qingshangongzai/Color_Card/contents/app_log/changelog.json?ref=main",
+ "https://gitcode.com/api/v5/repos/qingshangongzai/Color_Card/contents/docs/changelog.json?ref=main",
+ ],
+ ),
+ _GitHubSource(),
+]
+
+
+class _AssetSelector:
+ """安装包选择器"""
+
+ def select(self, assets: list[dict]) -> str:
+ """选择对应安装包
+
+ Args:
+ assets: 资源列表
+
+ Returns:
+ str: 下载链接,未找到返回空字符串
+ """
+ if not assets:
+ return ""
+
+ mode = get_app_mode()
+ platform = get_platform()
+
+ for asset in assets:
+ name = asset.get("name", "").lower()
+ url = asset.get("browser_download_url", "")
+
+ if not url:
+ continue
+
+ if platform == Platform.MACOS and name.endswith(".dmg"):
+ return url
+
+ if platform == Platform.WINDOWS:
+ if mode == AppMode.INSTALLED and "setup" in name and name.endswith(".exe"):
+ return url
+ if mode != AppMode.INSTALLED and name.endswith("x64.exe") and "setup" not in name:
+ return url
+
+ return assets[0].get("browser_download_url", "") if assets else ""
+
+
+class UpdateChecker(QThread):
+ """检查更新的后台线程"""
+
+ check_finished = Signal(CheckResult)
+
+ def __init__(self, current_version: str):
+ super().__init__()
+ self._current_version = current_version
+
+ def run(self):
+ """在后台线程中检查更新"""
+ last_error = ""
+
+ for source in _SOURCES:
+ try:
+ release = source.fetch_release()
+ latest_version = release.get("version", "")
+
+ if not latest_version:
+ last_error = tr("dialogs.update.error_parse_version")
+ continue
+
+ download_url = _AssetSelector().select(release.get("assets", []))
+ changelog = source.fetch_changelog(self._current_version, latest_version)
+
+ has_update = _compare_versions(self._current_version, latest_version) < 0
+ info = UpdateInfo(latest_version, download_url, changelog)
+
+ self.check_finished.emit(
+ CheckResult(True, has_update, info=info, current_version=self._current_version)
+ )
+ return
+
+ except requests.exceptions.Timeout:
+ last_error = tr("dialogs.update.error_timeout")
+ except requests.exceptions.ConnectionError:
+ last_error = tr("dialogs.update.error_connection")
+ except requests.exceptions.HTTPError:
+ last_error = tr("dialogs.update.error_http", status_code="")
+ except (requests.exceptions.RequestException, json.JSONDecodeError, KeyError) as e:
+ last_error = tr("dialogs.update.error_general", error=str(e))
+
+ self.check_finished.emit(CheckResult(False, False, error_message=last_error))
+
+
+class UpdateService:
+ """更新服务,协调检查更新流程"""
+
+ def __init__(self):
+ self._checker: UpdateChecker | None = None
+
+ def check_update(self, parent, current_version: str):
+ """检查更新并显示相应提示
+
+ Args:
+ parent: 父窗口对象
+ current_version: 当前版本号
+ """
+ self._checker = UpdateChecker(current_version)
+ self._checker.check_finished.connect(
+ lambda result: self._on_check_finished(result, parent)
+ )
+ self._checker.start()
+
+ def _on_check_finished(self, result: CheckResult, parent):
+ """处理检查结果
+
+ Args:
+ result: 检查结果
+ parent: 父窗口对象
+ """
+ from qfluentwidgets import InfoBar, InfoBarPosition
+ from dialogs import UpdateAvailableDialog
+
+ if not result.success:
+ InfoBar.warning(
+ title=tr("dialogs.update.check_failed"),
+ content=result.error_message,
+ parent=parent,
+ duration=5000,
+ position=InfoBarPosition.TOP,
+ )
+ return
+
+ if not result.has_update:
+ InfoBar.success(
+ title=tr("dialogs.update.info"),
+ content=tr("dialogs.update.latest_version"),
+ parent=parent,
+ duration=3000,
+ position=InfoBarPosition.TOP,
+ )
+ return
+
+ top_parent = parent.window() if parent else None
+ info = result.info
+ dialog = UpdateAvailableDialog(
+ top_parent,
+ current_version=result.current_version,
+ latest_version=info.latest_version,
+ download_url=info.download_url,
+ changelog=info.changelog,
+ )
+ dialog.exec()
diff --git a/dialogs/update_dialog.py b/dialogs/update_dialog.py
index ec38e236f913c33fdebbf50d7243b1ac45650a75..b8a6ef486d9a07c16632b579821c03bc4bdc895c 100644
--- a/dialogs/update_dialog.py
+++ b/dialogs/update_dialog.py
@@ -1,245 +1,17 @@
from __future__ import annotations
# 标准库导入
-import base64
-import json
-import re
-
+from typing import Any
# 第三方库导入
-import requests
-from PySide6.QtCore import Qt, QThread, Signal, QUrl
+from PySide6.QtCore import Qt, QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
-from qfluentwidgets import InfoBar, InfoBarPosition, PrimaryPushButton, PushButton, ScrollArea, ScrollBarHandleDisplayMode, qconfig
+from qfluentwidgets import PrimaryPushButton, PushButton, ScrollArea, ScrollBarHandleDisplayMode, qconfig
# 项目模块导入
from utils import tr, load_icon_universal
from utils.theme_colors import get_text_color, get_secondary_text_color
from dialogs import BaseFramelessDialog
-from core import get_app_mode, get_platform, AppMode, Platform
-
-
-class UpdateCheckThread(QThread):
- """检查更新的后台线程
-
- 在后台线程中检查 Gitee 仓库的最新版本信息,
- 避免阻塞主线程。
- """
-
- check_finished = Signal(bool, str, str, str, list)
-
- def __init__(self, current_version):
- """初始化检查更新线程
-
- Args:
- current_version: 当前版本号
- """
- super().__init__()
- self.current_version = current_version
-
- def run(self):
- """在后台线程中检查更新"""
- try:
- # 获取最新 Release 信息
- api_url = "https://gitee.com/api/v5/repos/qingshangongzai/Color_Card/releases/latest"
- response = requests.get(api_url, timeout=10)
-
- if response.status_code == 200:
- data = response.json()
- latest_version = data.get("tag_name", "").lstrip("v")
- download_url = self._select_asset(data.get("assets", []))
-
- # 获取 changelog.json 内容
- changelog_content = self._fetch_changelog(self.current_version, latest_version)
-
- if latest_version:
- self.check_finished.emit(True, latest_version, "", download_url, changelog_content)
- else:
- self.check_finished.emit(False, "", tr('dialogs.update.error_parse_version'), "", "")
- else:
- self.check_finished.emit(
- False, "", tr('dialogs.update.error_http', status_code=response.status_code), "", ""
- )
-
- except requests.exceptions.Timeout:
- self.check_finished.emit(False, "", tr('dialogs.update.error_timeout'), "", "")
- except requests.exceptions.ConnectionError:
- self.check_finished.emit(False, "", tr('dialogs.update.error_connection'), "", "")
- except Exception as e:
- self.check_finished.emit(False, "", tr('dialogs.update.error_general', error=str(e)), "", "")
-
- def _select_asset(self, assets):
- """选择对应安装包
-
- Args:
- assets: 资源列表
-
- Returns:
- str: 下载链接,未找到返回空字符串
- """
- if not assets:
- return ""
-
- mode = get_app_mode()
- platform = get_platform()
-
- for asset in assets:
- name = asset.get("name", "").lower()
- url = asset.get("browser_download_url", "")
-
- if not url:
- continue
-
- # macOS: 匹配 .dmg 文件
- if platform == Platform.MACOS and name.endswith(".dmg"):
- return url
-
- # Windows: 根据模式匹配
- if platform == Platform.WINDOWS:
- # 安装版: 匹配包含 setup 的 .exe
- if mode == AppMode.INSTALLED and "setup" in name and name.endswith(".exe"):
- return url
- # 便携版/其他: 匹配以 x64.exe 结尾且不含 setup 的
- if mode != AppMode.INSTALLED and name.endswith("x64.exe") and "setup" not in name:
- return url
-
- # 默认返回第一个
- return assets[0].get("browser_download_url", "") if assets else ""
-
- def _fetch_changelog(self, current_version: str, latest_version: str) -> list[Dict]:
- """从 Gitee 获取 changelog.json 并提取更新日志
-
- Args:
- current_version: 当前版本号
- latest_version: 最新版本号
-
- Returns:
- list[Dict]: 版本信息列表
- """
- urls = [
- "https://gitee.com/api/v5/repos/qingshangongzai/Color_Card/contents/app_log/changelog.json?ref=main",
- "https://gitee.com/api/v5/repos/qingshangongzai/Color_Card/contents/docs/changelog.json?ref=main"
- ]
-
- for url in urls:
- try:
- response = requests.get(url, timeout=10)
-
- if response.status_code != 200:
- continue
-
- data = response.json()
- content = data.get("content", "")
-
- json_content = base64.b64decode(content).decode('utf-8')
- changelog_data = json.loads(json_content)
-
- return self._format_changelog(changelog_data, current_version)
-
- except (requests.exceptions.RequestException, json.JSONDecodeError, KeyError):
- continue
-
- return []
-
- def _format_changelog(self, changelog_data: Dict, current_version: str) -> list[Dict]:
- """格式化更新日志
-
- Args:
- changelog_data: changelog.json 解析后的数据
- current_version: 当前版本号
-
- Returns:
- list[Dict]: 版本信息列表,每个版本包含 version、date、changes
- """
- versions = changelog_data.get("versions", [])
-
- # 收集需要显示的版本(从当前版本之后到最新版本)
- versions_to_show = []
- for version_info in versions:
- version_str = version_info.get("version", "").lstrip("v")
- if compare_versions(current_version, version_str) < 0:
- versions_to_show.append(version_info)
-
- return versions_to_show
-
-
-_PRE_RELEASE_ORDER = {"alpha": -3, "beta": -2, "rc": -1}
-
-
-def _parse_version(version_str: str) -> tuple[list[int], int, int]:
- """解析版本号为数字列表、预发布标识和预发布版本号
-
- Args:
- version_str: 版本号字符串
-
- Returns:
- tuple[list[int], int, int]: (版本号数字列表, 预发布标识, 预发布版本号)
- 预发布标识: 0=正式版, -1=RC, -2=Beta, -3=Alpha
- 预发布版本号: Beta1/Beta2等后面的数字,默认0
- """
- version_str = version_str.lstrip("v").lower()
-
- # 分离主版本号和预发布部分(处理 · 符号)
- # "1.7.0 · beta 1" -> main_part="1.7.0", pre_part="beta 1"
- if " · " in version_str:
- main_part, pre_part = version_str.split(" · ", 1)
- else:
- main_part = version_str
- pre_part = ""
-
- # 提取主版本号的数字
- parts = re.findall(r"\d+", main_part)
- nums = [int(p) for p in parts] if parts else [0]
-
- # 解析预发布标识
- pre_release = 0
- pre_release_num = 0
- for keyword, value in _PRE_RELEASE_ORDER.items():
- if keyword in pre_part:
- pre_release = value
- # 支持 "beta1" 和 "beta 1" 两种格式
- match = re.search(rf"{keyword}\s*(\d+)", pre_part)
- if match:
- pre_release_num = int(match.group(1))
- break
-
- return nums, pre_release, pre_release_num
-
-
-def compare_versions(current: str, latest: str) -> int:
- """比较版本号
-
- Args:
- current: 当前版本号
- latest: 最新版本号
-
- Returns:
- int: 0表示版本相同,1表示当前版本更新,-1表示有新版本
- """
- current_parts, current_pre, current_pre_num = _parse_version(current)
- latest_parts, latest_pre, latest_pre_num = _parse_version(latest)
-
- max_len = max(len(current_parts), len(latest_parts))
- current_parts.extend([0] * (max_len - len(current_parts)))
- latest_parts.extend([0] * (max_len - len(latest_parts)))
-
- for c, latest_part in zip(current_parts, latest_parts):
- if c > latest_part:
- return 1
- elif c < latest_part:
- return -1
-
- if current_pre > latest_pre:
- return 1
- elif current_pre < latest_pre:
- return -1
-
- if current_pre_num > latest_pre_num:
- return 1
- elif current_pre_num < latest_pre_num:
- return -1
-
- return 0
class UpdateAvailableDialog(BaseFramelessDialog):
@@ -248,8 +20,6 @@ class UpdateAvailableDialog(BaseFramelessDialog):
当检测到有新版本时弹出,提供直接下载或跳转到发行页面的功能。
"""
- _check_thread = None # 类变量,用于保存检查更新的线程对象
-
def __init__(self, parent=None, current_version="", latest_version="", download_url="", changelog=None):
"""初始化新版本提示对话框
@@ -262,7 +32,7 @@ class UpdateAvailableDialog(BaseFramelessDialog):
"""
super().__init__(parent)
self.setWindowTitle(tr('dialogs.update.title'))
- self.setFixedSize(700, 550)
+ self.setFixedSize(450, 500)
self.current_version = current_version
self.latest_version = latest_version
self.download_url = download_url
@@ -286,7 +56,7 @@ class UpdateAvailableDialog(BaseFramelessDialog):
# 样式准备好后允许显示
self._enable_show()
- def _changelog_to_html(self, versions: list[Dict]) -> str:
+ def _changelog_to_html(self, versions: list[dict[str, Any]]) -> str:
"""将版本信息列表转换为 HTML
Args:
@@ -335,9 +105,7 @@ class UpdateAvailableDialog(BaseFramelessDialog):
elif isinstance(item, dict):
title = item.get("title", "")
desc = item.get("desc", "")
- # 子标题单独一行显示,不加冒号
html_lines.append(f'