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'
{title}
') - # 按
分割描述,每行单独渲染 desc_lines = desc.split("
") for line in desc_lines: html_lines.append(f'
{line}
') @@ -348,7 +116,6 @@ class UpdateAvailableDialog(BaseFramelessDialog): """更新样式以适配主题""" super()._update_styles() - # 更新更新日志标签样式 if hasattr(self, 'changelog_label') and self.changelog_label: text_color = get_text_color().name() self.changelog_label.setStyleSheet(f""" @@ -362,23 +129,21 @@ class UpdateAvailableDialog(BaseFramelessDialog): def setup_ui(self): """设置界面布局""" layout = QVBoxLayout(self) - # 顶部边距40px为标题栏留出空间 layout.setContentsMargins(20, 40, 20, 20) layout.setSpacing(15) - # 提示文本(基类统一处理文字颜色) + # 提示文本 info_label = QLabel(tr('dialogs.update.new_version')) info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(info_label) - # 版本信息(基类统一处理文字颜色) + # 版本信息 version_label = QLabel(tr('dialogs.update.version_info', current=self.current_version, latest=self.latest_version)) version_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(version_label) # 更新日志区域 if self.changelog: - # 创建 ScrollArea 包装 QLabel scroll_area = ScrollArea() scroll_area.scrollDelagate.vScrollBar.setHandleDisplayMode(ScrollBarHandleDisplayMode.ON_HOVER) scroll_area.setWidgetResizable(True) @@ -393,15 +158,12 @@ class UpdateAvailableDialog(BaseFramelessDialog): } """) - # 设置滚动条角落为透明(防止出现灰色方块) corner_widget = QWidget() corner_widget.setStyleSheet("background: transparent;") scroll_area.setCornerWidget(corner_widget) - # 将版本信息转换为 HTML html_content = self._changelog_to_html(self.changelog) - # 创建 QLabel 显示 HTML 内容 self.changelog_label = QLabel() self.changelog_label.setTextFormat(Qt.TextFormat.RichText) self.changelog_label.setText(html_content) @@ -409,7 +171,6 @@ class UpdateAvailableDialog(BaseFramelessDialog): self.changelog_label.setOpenExternalLinks(True) self.changelog_label.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) - # 设置透明背景和文字颜色 text_color = get_text_color().name() self.changelog_label.setStyleSheet(f""" QLabel {{ @@ -432,13 +193,11 @@ class UpdateAvailableDialog(BaseFramelessDialog): buttons_layout.addStretch() - # 发行页面按钮 release_button = PushButton(tr('dialogs.update.release_page')) release_button.setMinimumWidth(90) release_button.clicked.connect(self.open_release_page) buttons_layout.addWidget(release_button) - # 下载按钮(主题色) download_button = PrimaryPushButton(tr('dialogs.update.download')) download_button.setMinimumWidth(90) download_button.clicked.connect(self.on_download_clicked) @@ -453,7 +212,6 @@ class UpdateAvailableDialog(BaseFramelessDialog): if self.download_url: QDesktopServices.openUrl(QUrl(self.download_url)) else: - # 降级到发行页面 self.open_release_page() self.accept() @@ -468,45 +226,4 @@ class UpdateAvailableDialog(BaseFramelessDialog): def closeEvent(self, event): """关闭事件""" - super().closeEvent(event) # 基类处理信号断开 - - @staticmethod - def check_update(parent, current_version): - """检查更新并显示相应提示 - - Args: - parent: 父窗口对象 - current_version: 当前版本号 - """ - # 创建并启动检查线程 - UpdateAvailableDialog._check_thread = UpdateCheckThread(current_version) - - def on_check_finished(success, latest_version, error_msg, download_url, changelog=None): - if success: - result = compare_versions(current_version, latest_version) - if result >= 0: - # 当前版本已是最新 - InfoBar.success( - title=tr('dialogs.update.info'), - content=tr('dialogs.update.latest_version'), - parent=parent, - duration=3000, - position=InfoBarPosition.TOP, - ) - else: - # 有新版本可用,显示对话框 - # 使用 window() 获取顶层窗口,确保无边框对话框正常显示 - top_parent = parent.window() if parent else None - dialog = UpdateAvailableDialog(top_parent, current_version, latest_version, download_url, changelog) - dialog.exec() - else: - InfoBar.warning( - title=tr('dialogs.update.check_failed'), - content=error_msg, - parent=parent, - duration=5000, - position=InfoBarPosition.TOP, - ) - - UpdateAvailableDialog._check_thread.check_finished.connect(on_check_finished) - UpdateAvailableDialog._check_thread.start() + super().closeEvent(event) diff --git a/docs/changelog.json b/docs/changelog.json index a3197b3591dff2b07ac4cdb399ad502df866a0e7..124e6615cf4e8b98996c5275217c9772f1cad57b 100644 --- a/docs/changelog.json +++ b/docs/changelog.json @@ -1,5 +1,32 @@ { "versions": [ + { + "version": "v1.10.2", + "date": "2026-05-31", + "changes": [ + { + "category": "问题修复", + "items": [ + "修复更换图片时未清空旧数据,导致显示上一张图片直方图的问题", + "修复明度分析面板隐藏采样点状态在更换图片后失效的问题" + ] + }, + { + "category": "体验优化", + "items": [ + "优化明度遮罩与直方图使用一致的 Gamma 校正算法,避免两者显示不一致", + "优化更新日志逻辑,最新正式版时隐藏预发布版本日志", + "更新服务新增多平台备用方案" + ] + }, + { + "category": "代码重构", + "items": [ + "优化更新检查模块代码" + ] + } + ] + }, { "version": "v1.10.1", "date": "2026-05-24", @@ -596,9 +623,13 @@ "title": "内置色彩面板", "desc": "新增内置色彩面板,集成大量开源配色方案,包含 Open Color、Nice Color Palettes、Tailwind CSS Colors 等知名配色库,支持配色随机模式、收藏到配色管理面板、预览功能" }, + { + "title": "编辑配色对话框", + "desc": "新增编辑配色对话框,支持编辑配色" + }, { "title": "配色管理增强", - "desc": "为色卡收藏面板添加16进制颜色值编辑功能,新增编辑配色对话框,支持添加和编辑配色" + "desc": "为色卡收藏面板添加16进制颜色值编辑功能
新增「添加」按钮,支持连接编辑配色对话框" }, { "title": "启动体验优化", @@ -641,8 +672,7 @@ "category": "代码重构", "items": [ "将色卡收藏面板重命名为配色管理,将配色方案面板重命名为配色生成", - "统一配色管理导入导出JSON格式", - "新增编辑配色对话框" + "统一配色管理导入导出JSON格式" ] } ] diff --git a/installer/main.py b/installer/main.py index e7eb8b0696a64f0e7e229486e3374b30b0c58340..64e935e474b961c56476272d0ac1e2996c388eef 100644 --- a/installer/main.py +++ b/installer/main.py @@ -357,8 +357,9 @@ def run_main_app(): except (ValueError, TypeError): pass from version import version_manager - from dialogs import UpdateAvailableDialog - UpdateAvailableDialog.check_update(window, version_manager.get_version()) + from core.update_service import UpdateService + update_service = UpdateService() + update_service.check_update(window, version_manager.get_version()) config_manager.set('settings.last_check_time', datetime.now().isoformat()) config_manager.save() diff --git a/main.py b/main.py index c8e7ed6f5f856b7144c784496372672990e112bb..ff7e5fce67ae559d3b0b1f47b54075510083c092 100644 --- a/main.py +++ b/main.py @@ -243,8 +243,9 @@ def main(): pass from version import version_manager - from dialogs import UpdateAvailableDialog - UpdateAvailableDialog.check_update(window, version_manager.get_version()) + from core.update_service import UpdateService + update_service = UpdateService() + update_service.check_update(window, version_manager.get_version()) config_manager.set('settings.last_check_time', datetime.now().isoformat()) config_manager.save() diff --git a/tests/test_update_service.py b/tests/test_update_service.py new file mode 100644 index 0000000000000000000000000000000000000000..3b218c2ff8d3624451cb9398e902a56dbf4f223b --- /dev/null +++ b/tests/test_update_service.py @@ -0,0 +1,545 @@ +from __future__ import annotations +import sys +import unittest +from unittest.mock import patch, MagicMock +from dataclasses import dataclass + +sys.path.insert(0, '.') + + +class TestParseVersion(unittest.TestCase): + """测试版本解析函数""" + + def test_parse_simple_version(self): + """测试简单版本号解析""" + from core.update_service import _parse_version + + nums, pre, pre_num = _parse_version("1.0.0") + self.assertEqual(nums, [1, 0, 0]) + self.assertEqual(pre, 0) + self.assertEqual(pre_num, 0) + + def test_parse_version_with_v_prefix(self): + """测试带 v 前缀的版本号""" + from core.update_service import _parse_version + + nums, pre, pre_num = _parse_version("v2.1.3") + self.assertEqual(nums, [2, 1, 3]) + self.assertEqual(pre, 0) + self.assertEqual(pre_num, 0) + + def test_parse_alpha_version(self): + """测试 Alpha 版本解析""" + from core.update_service import _parse_version + + nums, pre, pre_num = _parse_version("1.0.0-alpha") + self.assertEqual(nums, [1, 0, 0]) + self.assertEqual(pre, -3) + self.assertEqual(pre_num, 0) + + def test_parse_alpha_with_number(self): + """测试带数字的 Alpha 版本""" + from core.update_service import _parse_version + + nums, pre, pre_num = _parse_version("1.0.0-alpha2") + self.assertEqual(nums, [1, 0, 0]) + self.assertEqual(pre, -3) + self.assertEqual(pre_num, 2) + + def test_parse_beta_version(self): + """测试 Beta 版本解析""" + from core.update_service import _parse_version + + nums, pre, pre_num = _parse_version("1.0.0-beta1") + self.assertEqual(nums, [1, 0, 0]) + self.assertEqual(pre, -2) + self.assertEqual(pre_num, 1) + + def test_parse_rc_version(self): + """测试 RC 版本解析""" + from core.update_service import _parse_version + + nums, pre, pre_num = _parse_version("1.0.0-rc3") + self.assertEqual(nums, [1, 0, 0]) + self.assertEqual(pre, -1) + self.assertEqual(pre_num, 3) + + def test_parse_version_with_dot_separator(self): + """测试使用 · 分隔符的版本号""" + from core.update_service import _parse_version + + nums, pre, pre_num = _parse_version("1.0.0 · Beta1") + self.assertEqual(nums, [1, 0, 0]) + self.assertEqual(pre, -2) + self.assertEqual(pre_num, 1) + + def test_parse_version_with_space_separator(self): + """测试使用空格分隔符的版本号""" + from core.update_service import _parse_version + + nums, pre, pre_num = _parse_version("1.0.0 RC2") + self.assertEqual(nums, [1, 0, 0]) + self.assertEqual(pre, -1) + self.assertEqual(pre_num, 2) + + +class TestCompareVersions(unittest.TestCase): + """测试版本比较函数""" + + def test_same_versions(self): + """测试相同版本""" + from core.update_service import _compare_versions + + result = _compare_versions("1.0.0", "1.0.0") + self.assertEqual(result, 0) + + def test_current_older_major(self): + """测试主版本号更旧""" + from core.update_service import _compare_versions + + result = _compare_versions("1.0.0", "2.0.0") + self.assertEqual(result, -1) + + def test_current_older_minor(self): + """测试次版本号更旧""" + from core.update_service import _compare_versions + + result = _compare_versions("1.0.0", "1.1.0") + self.assertEqual(result, -1) + + def test_current_older_patch(self): + """测试补丁版本号更旧""" + from core.update_service import _compare_versions + + result = _compare_versions("1.0.0", "1.0.1") + self.assertEqual(result, -1) + + def test_current_newer(self): + """测试当前版本更新""" + from core.update_service import _compare_versions + + result = _compare_versions("2.0.0", "1.0.0") + self.assertEqual(result, 1) + + def test_release_vs_alpha(self): + """测试正式版比 Alpha 新""" + from core.update_service import _compare_versions + + result = _compare_versions("1.0.0", "1.0.0-alpha") + self.assertEqual(result, 1) + + def test_alpha_vs_beta(self): + """测试 Alpha 比 Beta 旧""" + from core.update_service import _compare_versions + + result = _compare_versions("1.0.0-alpha", "1.0.0-beta") + self.assertEqual(result, -1) + + def test_beta_vs_rc(self): + """测试 Beta 比 RC 旧""" + from core.update_service import _compare_versions + + result = _compare_versions("1.0.0-beta", "1.0.0-rc") + self.assertEqual(result, -1) + + def test_rc_vs_release(self): + """测试 RC 比正式版旧""" + from core.update_service import _compare_versions + + result = _compare_versions("1.0.0-rc", "1.0.0") + self.assertEqual(result, -1) + + def test_alpha1_vs_alpha2(self): + """测试 Alpha1 比 Alpha2 旧""" + from core.update_service import _compare_versions + + result = _compare_versions("1.0.0-alpha1", "1.0.0-alpha2") + self.assertEqual(result, -1) + + def test_different_length_versions(self): + """测试不同长度的版本号""" + from core.update_service import _compare_versions + + result = _compare_versions("1.0", "1.0.1") + self.assertEqual(result, -1) + + def test_complex_version_comparison(self): + """测试复杂版本比较""" + from core.update_service import _compare_versions + + result = _compare_versions("1.9.0", "1.10.0") + self.assertEqual(result, -1) + + +class TestChangelogFetcherFormat(unittest.TestCase): + """测试更新日志格式化""" + + def test_format_filters_old_versions(self): + """测试过滤旧版本""" + from core.update_service import _ChangelogFetcher + + fetcher = _ChangelogFetcher() + changelog_data = { + "versions": [ + {"version": "1.0.0", "date": "2024-01-01", "changes": []}, + {"version": "1.1.0", "date": "2024-02-01", "changes": []}, + {"version": "1.2.0", "date": "2024-03-01", "changes": []}, + ] + } + + result = fetcher._format(changelog_data, "1.0.0", "1.2.0") + + self.assertEqual(len(result), 2) + self.assertEqual(result[0]["version"], "1.1.0") + self.assertEqual(result[1]["version"], "1.2.0") + + def test_format_filters_pre_release_when_latest_is_release(self): + """测试正式版发布时过滤预发布版本""" + from core.update_service import _ChangelogFetcher + + fetcher = _ChangelogFetcher() + changelog_data = { + "versions": [ + {"version": "1.0.0", "date": "2024-01-01", "changes": []}, + {"version": "1.1.0-beta1", "date": "2024-02-01", "changes": []}, + {"version": "1.1.0", "date": "2024-03-01", "changes": []}, + ] + } + + result = fetcher._format(changelog_data, "1.0.0", "1.1.0") + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["version"], "1.1.0") + + def test_format_includes_pre_release_when_latest_is_pre_release(self): + """测试预发布版本时包含预发布版本""" + from core.update_service import _ChangelogFetcher + + fetcher = _ChangelogFetcher() + changelog_data = { + "versions": [ + {"version": "1.0.0", "date": "2024-01-01", "changes": []}, + {"version": "1.1.0-alpha1", "date": "2024-02-01", "changes": []}, + {"version": "1.1.0-beta1", "date": "2024-03-01", "changes": []}, + ] + } + + result = fetcher._format(changelog_data, "1.0.0", "1.1.0-beta1") + + self.assertEqual(len(result), 2) + + def test_format_empty_changelog(self): + """测试空更新日志""" + from core.update_service import _ChangelogFetcher + + fetcher = _ChangelogFetcher() + changelog_data = {"versions": []} + + result = fetcher._format(changelog_data, "1.0.0", "1.1.0") + + self.assertEqual(result, []) + + def test_format_all_versions_current(self): + """测试所有版本都是当前版本""" + from core.update_service import _ChangelogFetcher + + fetcher = _ChangelogFetcher() + changelog_data = { + "versions": [ + {"version": "1.0.0", "date": "2024-01-01", "changes": []}, + {"version": "1.1.0", "date": "2024-02-01", "changes": []}, + ] + } + + result = fetcher._format(changelog_data, "1.1.0", "1.1.0") + + self.assertEqual(result, []) + + +class TestAssetSelector(unittest.TestCase): + """测试安装包选择器""" + + def test_select_windows_installed_mode(self): + """测试 Windows 安装模式选择""" + from core.update_service import _AssetSelector + + assets = [ + {"name": "color_card_1.0.0_x64.exe", "browser_download_url": "url_portable"}, + {"name": "color_card_1.0.0_setup.exe", "browser_download_url": "url_setup"}, + ] + + with patch('core.update_service.get_app_mode', return_value=MagicMock(INSTALLED=True)): + with patch('core.update_service.get_platform', return_value=MagicMock(WINDOWS=True, MACOS=False)): + with patch('core.update_service.AppMode') as mock_mode: + with patch('core.update_service.Platform') as mock_platform: + mock_mode.INSTALLED = MagicMock() + mock_mode.INSTALLED = True + mock_platform.WINDOWS = True + mock_platform.MACOS = False + + selector = _AssetSelector() + result = selector.select(assets) + + self.assertEqual(result, "url_portable") + + def test_select_empty_assets(self): + """测试空资源列表""" + from core.update_service import _AssetSelector + + selector = _AssetSelector() + result = selector.select([]) + + self.assertEqual(result, "") + + def test_select_fallback_to_first(self): + """测试回退到第一个资源""" + from core.update_service import _AssetSelector + + assets = [ + {"name": "unknown_file.zip", "browser_download_url": "url_first"}, + ] + + with patch('core.update_service.get_app_mode') as mock_mode: + with patch('core.update_service.get_platform') as mock_platform: + with patch('core.update_service.AppMode'): + with patch('core.update_service.Platform') as mock_p: + mock_p.WINDOWS = False + mock_p.MACOS = False + + selector = _AssetSelector() + result = selector.select(assets) + + self.assertEqual(result, "url_first") + + +class TestDataClasses(unittest.TestCase): + """测试数据类""" + + def test_update_info_creation(self): + """测试 UpdateInfo 创建""" + from core.update_service import UpdateInfo + + info = UpdateInfo( + latest_version="1.1.0", + download_url="https://example.com/download", + changelog=[{"version": "1.1.0"}] + ) + + self.assertEqual(info.latest_version, "1.1.0") + self.assertEqual(info.download_url, "https://example.com/download") + self.assertEqual(len(info.changelog), 1) + + def test_check_result_creation(self): + """测试 CheckResult 创建""" + from core.update_service import CheckResult, UpdateInfo + + info = UpdateInfo("1.1.0", "url", []) + result = CheckResult( + success=True, + has_update=True, + info=info, + current_version="1.0.0" + ) + + self.assertTrue(result.success) + self.assertTrue(result.has_update) + self.assertEqual(result.current_version, "1.0.0") + self.assertIsNotNone(result.info) + + def test_check_result_defaults(self): + """测试 CheckResult 默认值""" + from core.update_service import CheckResult + + result = CheckResult(success=False, has_update=False) + + self.assertIsNone(result.info) + self.assertEqual(result.error_message, "") + self.assertEqual(result.current_version, "") + + +class TestUpdateChecker(unittest.TestCase): + """测试更新检查线程""" + + def test_check_success(self): + """测试成功检查更新""" + from PySide6.QtCore import QCoreApplication, QEventLoop + from core.update_service import UpdateChecker, CheckResult + + app = QCoreApplication.instance() or QCoreApplication([]) + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "tag_name": "v1.1.0", + "assets": [ + {"name": "app_1.1.0_x64.exe", "browser_download_url": "https://example.com/download"} + ] + } + + results = [] + + def on_finished(result: CheckResult): + results.append(result) + + with patch('requests.get', return_value=mock_response): + with patch('core.update_service._ChangelogFetcher.fetch', return_value=[]): + with patch('core.update_service._AssetSelector.select', return_value="https://example.com/download"): + checker = UpdateChecker("1.0.0") + checker.check_finished.connect(on_finished) + checker.run() + + self.assertEqual(len(results), 1) + self.assertTrue(results[0].success) + self.assertTrue(results[0].has_update) + self.assertEqual(results[0].current_version, "1.0.0") + + def test_check_no_update(self): + """测试无更新""" + from PySide6.QtCore import QCoreApplication + from core.update_service import UpdateChecker, CheckResult + + app = QCoreApplication.instance() or QCoreApplication([]) + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "tag_name": "v1.0.0", + "assets": [] + } + + results = [] + + def on_finished(result: CheckResult): + results.append(result) + + with patch('requests.get', return_value=mock_response): + with patch('core.update_service._ChangelogFetcher.fetch', return_value=[]): + checker = UpdateChecker("1.0.0") + checker.check_finished.connect(on_finished) + checker.run() + + self.assertEqual(len(results), 1) + self.assertTrue(results[0].success) + self.assertFalse(results[0].has_update) + + def test_check_http_error(self): + """测试 HTTP 错误""" + from PySide6.QtCore import QCoreApplication + from core.update_service import UpdateChecker, CheckResult + + app = QCoreApplication.instance() or QCoreApplication([]) + + mock_response = MagicMock() + mock_response.status_code = 404 + + results = [] + + def on_finished(result: CheckResult): + results.append(result) + + with patch('requests.get', return_value=mock_response): + checker = UpdateChecker("1.0.0") + checker.check_finished.connect(on_finished) + checker.run() + + self.assertEqual(len(results), 1) + self.assertFalse(results[0].success) + self.assertFalse(results[0].has_update) + self.assertIn("error_http", results[0].error_message) + + def test_check_timeout(self): + """测试超时错误""" + from PySide6.QtCore import QCoreApplication + from core.update_service import UpdateChecker, CheckResult + import requests + + app = QCoreApplication.instance() or QCoreApplication([]) + + results = [] + + def on_finished(result: CheckResult): + results.append(result) + + with patch('requests.get', side_effect=requests.exceptions.Timeout()): + checker = UpdateChecker("1.0.0") + checker.check_finished.connect(on_finished) + checker.run() + + self.assertEqual(len(results), 1) + self.assertFalse(results[0].success) + self.assertFalse(results[0].has_update) + + def test_check_connection_error(self): + """测试连接错误""" + from PySide6.QtCore import QCoreApplication + from core.update_service import UpdateChecker, CheckResult + import requests + + app = QCoreApplication.instance() or QCoreApplication([]) + + results = [] + + def on_finished(result: CheckResult): + results.append(result) + + with patch('requests.get', side_effect=requests.exceptions.ConnectionError()): + checker = UpdateChecker("1.0.0") + checker.check_finished.connect(on_finished) + checker.run() + + self.assertEqual(len(results), 1) + self.assertFalse(results[0].success) + + +class TestUpdateService(unittest.TestCase): + """测试更新服务""" + + def test_service_creation(self): + """测试服务创建""" + from core.update_service import UpdateService + + service = UpdateService() + self.assertIsNone(service._checker) + + def test_check_update_creates_checker(self): + """测试检查更新创建检查器""" + from PySide6.QtCore import QCoreApplication + from core.update_service import UpdateService + + app = QCoreApplication.instance() or QCoreApplication([]) + + mock_parent = MagicMock() + + with patch('core.update_service.UpdateChecker') as mock_checker_class: + mock_checker = MagicMock() + mock_checker_class.return_value = mock_checker + + service = UpdateService() + service.check_update(mock_parent, "1.0.0") + + mock_checker_class.assert_called_once_with("1.0.0") + mock_checker.start.assert_called_once() + self.assertEqual(service._checker, mock_checker) + + +def run_tests(): + """运行所有测试""" + loader = unittest.TestLoader() + suite = unittest.TestSuite() + + suite.addTests(loader.loadTestsFromTestCase(TestParseVersion)) + suite.addTests(loader.loadTestsFromTestCase(TestCompareVersions)) + suite.addTests(loader.loadTestsFromTestCase(TestChangelogFetcherFormat)) + suite.addTests(loader.loadTestsFromTestCase(TestAssetSelector)) + suite.addTests(loader.loadTestsFromTestCase(TestDataClasses)) + suite.addTests(loader.loadTestsFromTestCase(TestUpdateChecker)) + suite.addTests(loader.loadTestsFromTestCase(TestUpdateService)) + + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + return result + + +if __name__ == '__main__': + run_tests() diff --git a/ui/canvases.py b/ui/canvases.py index a43a8c3ad8ad549d0144ea136154ce94c143b59d..9d821e1e53de246393e21da66c1606ebcf51fc20 100644 --- a/ui/canvases.py +++ b/ui/canvases.py @@ -1355,9 +1355,9 @@ class LuminanceCanvas(BaseCanvas): emit_sync: 是否发射同步信号(从其他面板同步时设为False,防止循环) """ if self._original_pixmap and not self._original_pixmap.isNull(): - # 确保取色点可见 + # 根据用户设置控制取色点可见性 for picker in self._pickers: - picker.show() + picker.setVisible(self._pickers_visible) # 改变光标为默认 self.setCursor(Qt.CursorShape.ArrowCursor) diff --git a/ui/histograms.py b/ui/histograms.py index 17ccdf6942474f837bbe5ede2e9e36a90dc44675..8da7cb1cccc6b04c6d1e33e7ec7d589fe802a1d8 100644 --- a/ui/histograms.py +++ b/ui/histograms.py @@ -319,6 +319,9 @@ class LuminanceHistogramWidget(BaseHistogram): return self._current_image = image self.set_loading(True) + self._histogram = [] + self._max_count = 0 + self.update() self._histogram_service.calculate_luminance_async(image) def set_sampling_mode(self, mode: str): @@ -693,6 +696,11 @@ class RGBHistogramWidget(BaseHistogram): return self._current_image = image self.set_loading(True) + self._histogram_r = [0] * 256 + self._histogram_g = [0] * 256 + self._histogram_b = [0] * 256 + self._max_count = 0 + self.update() self._histogram_service.calculate_rgb_async(image) def set_sampling_mode(self, mode: str): @@ -1002,6 +1010,9 @@ class HueHistogramWidget(BaseHistogram): return self._current_image = image self.set_loading(True) + self._histogram = [0] * 360 + self._max_count = 0 + self.update() self._histogram_service.calculate_hue_async(image) def set_sampling_mode(self, mode: str): diff --git a/ui/settings.py b/ui/settings.py index 6ccc2c8177bcef7dd21f27b3f12fe9df99c0c9b5..151c6ebd45edb118b576cfb44e8598d9d7efe17f 100644 --- a/ui/settings.py +++ b/ui/settings.py @@ -11,7 +11,8 @@ from core import get_config_manager from core.logger import get_logger, log_user_action from utils import tr, get_supported_languages, set_language, get_locale_manager from utils.theme_colors import get_title_color -from dialogs import AboutDialog, UpdateAvailableDialog +from dialogs import AboutDialog +from core.update_service import UpdateService from version import version_manager logger = get_logger("settings") @@ -64,6 +65,7 @@ class SettingsInterface(QWidget): self._color_picker_mode = self._config_manager.get('settings.color_picker_mode', 'original') self._auto_check_update = self._config_manager.get('settings.auto_check_update', True) self._language = self._config_manager.get('settings.language', 'HY_JT') + self._update_service = UpdateService() self.setup_ui() self._update_styles() self._update_color_space_availability(self._gradient_mode) @@ -1050,7 +1052,7 @@ class SettingsInterface(QWidget): """检查更新按钮点击""" log_user_action("check_update") current_version = version_manager.get_version() - UpdateAvailableDialog.check_update(self, current_version) + self._update_service.check_update(self, current_version) def on_show_about(self): """显示关于对话框""" diff --git a/version.py b/version.py index 4300633312e97a39416eaa3c2baac7215b38680f..ecf92ec091317476ee9f1c1d25b7ed00b63ecf55 100644 --- a/version.py +++ b/version.py @@ -10,8 +10,8 @@ class VersionManager: # 版本号组件 self.major: int = 1 self.minor: int = 10 - self.patch: int = 1 - self.build: int = 1 + self.patch: int = 2 + self.build: int = 2 self.prerelease: str = "" # 核心版本信息 diff --git a/version.txt b/version.txt index 49aab8271c1ea7cb5379e6a90a2f8f7adb31e6be..1fd142495aeb5aad0edf1486db31339194774e9e 100644 --- a/version.txt +++ b/version.txt @@ -1,6 +1,6 @@ -1.10.1 -2026.5.24.2 -1.10.1.1 +1.10.2 +2026.5.31.1 +1.10.2.2 浮晓 HXiao Studio © 2026 浮晓 HXiao Studio 取色卡(Color Card) - 一站式色彩工具 \ No newline at end of file diff --git a/version_info.txt b/version_info.txt index 38dfc1d5864217d03124db5b5e4f2bcafd1dd54e..5cdd70a8d4806df8d00a15f08fc9172aec5ce502 100644 --- a/version_info.txt +++ b/version_info.txt @@ -1,7 +1,7 @@ VSVersionInfo( ffi=FixedFileInfo( - filevers=(2026,5,24,2), - prodvers=(1,10,1,1), + filevers=(2026,5,31,1), + prodvers=(1,10,2,2), mask=0x3f, flags=0x0, OS=0x4, @@ -17,12 +17,12 @@ VSVersionInfo( [ StringStruct(u'CompanyName', u'浮晓 HXiao Studio'), StringStruct(u'FileDescription', u'取色卡(Color Card) - 一站式色彩工具'), - StringStruct(u'FileVersion', u'1.10.1'), + StringStruct(u'FileVersion', u'1.10.2'), StringStruct(u'InternalName', u'Color_Card'), StringStruct(u'LegalCopyright', u'© 2026 浮晓 HXiao Studio'), StringStruct(u'OriginalFilename', u'Color_Card.exe'), StringStruct(u'ProductName', u'取色卡'), - StringStruct(u'ProductVersion', u'1.10.1'), + StringStruct(u'ProductVersion', u'1.10.2'), StringStruct(u'Comments', u'一站式的图片的图片分析和配色工具') ] ) diff --git a/website/public/changelog.json b/website/public/changelog.json index 00f7b24eae617cc3abbf9af7f820e607a4822df2..6d5e9ecae05dbdb3f02b8e995425a48109c6d380 100644 --- a/website/public/changelog.json +++ b/website/public/changelog.json @@ -1,5 +1,32 @@ { "versions": [ + { + "version": "v1.10.2", + "date": "2026-05-31", + "changes": [ + { + "category": "问题修复", + "items": [ + "修复更换图片时未清空旧数据,导致显示上一张图片直方图的问题", + "修复明度分析面板隐藏采样点状态在更换图片后失效的问题" + ] + }, + { + "category": "体验优化", + "items": [ + "优化明度遮罩与直方图使用一致的 Gamma 校正算法,避免两者显示不一致", + "优化更新日志逻辑,最新正式版时隐藏预发布版本日志", + "更新服务新增多平台备用方案" + ] + }, + { + "category": "代码重构", + "items": [ + "优化更新检查模块代码" + ] + } + ] + }, { "version": "v1.10.1", "date": "2026-05-24", @@ -596,9 +623,13 @@ "title": "内置色彩面板", "desc": "新增内置色彩面板,集成大量开源配色方案,包含 Open Color、Nice Color Palettes、Tailwind CSS Colors 等知名配色库,支持配色随机模式、收藏到配色管理面板、预览功能" }, + { + "title": "编辑配色对话框", + "desc": "新增编辑配色对话框,支持编辑配色" + }, { "title": "配色管理增强", - "desc": "为色卡收藏面板添加16进制颜色值编辑功能,新增编辑配色对话框,支持添加和编辑配色" + "desc": "为色卡收藏面板添加16进制颜色值编辑功能
新增「添加」按钮,支持连接编辑配色对话框" }, { "title": "启动体验优化", @@ -641,8 +672,7 @@ "category": "代码重构", "items": [ "将色卡收藏面板重命名为配色管理,将配色方案面板重命名为配色生成", - "统一配色管理导入导出JSON格式", - "新增编辑配色对话框" + "统一配色管理导入导出JSON格式" ] } ]