From af8455dc6db9de4b0f554500425b6ef75778cd97 Mon Sep 17 00:00:00 2001 From: superadminist <18433706+superadminist@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:08:21 +0800 Subject: [PATCH 1/2] =?UTF-8?q?ui:=20=E5=8F=91=E5=B8=83=20v1.11.4=20?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E4=B8=8B=E6=8B=89=E4=BA=A4=E4=BA=92=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 2 +- README.ja.md | 2 +- README.ko.md | 2 +- README.md | 2 +- README.zh-TW.md | 2 +- api/providers/codex.py | 2 +- app_identity.py | 2 +- installer/TokenMeter.iss | 2 +- release-notes/v1.11.4.md | 29 ++++++ tests/test_providers.py | 2 +- tests/test_qt_ui.py | 105 +++++++++++++++++++- ui/qt_panel.py | 206 +++++++++++++++++++++++++++++++-------- ui/qt_theme.py | 10 +- updater_version_info.txt | 4 +- version_info.txt | 4 +- 15 files changed, 317 insertions(+), 59 deletions(-) create mode 100644 release-notes/v1.11.4.md diff --git a/README.en.md b/README.en.md index 0f6d659..0cc4136 100644 --- a/README.en.md +++ b/README.en.md @@ -137,7 +137,7 @@ TokenMeter/ ## Version and releases -Current version: `1.11.3`. See [GitHub Releases](https://github.com/zensoku142/TokenMeter/releases) for change notes and checksums. +Current version: `1.11.4`. See [GitHub Releases](https://github.com/zensoku142/TokenMeter/releases) for change notes and checksums. ## License diff --git a/README.ja.md b/README.ja.md index dd7d23d..1579b8a 100644 --- a/README.ja.md +++ b/README.ja.md @@ -129,7 +129,7 @@ TokenMeter/ ## バージョンと Release -現在のバージョン:`1.11.3`。変更履歴とチェックサムは [GitHub Releases](https://github.com/zensoku142/TokenMeter/releases) を参照してください。 +現在のバージョン:`1.11.4`。変更履歴とチェックサムは [GitHub Releases](https://github.com/zensoku142/TokenMeter/releases) を参照してください。 ## License diff --git a/README.ko.md b/README.ko.md index e7b0b77..1488006 100644 --- a/README.ko.md +++ b/README.ko.md @@ -129,7 +129,7 @@ TokenMeter/ ## 버전 및 Release -현재 버전: `1.11.3`. 변경 사항과 체크섬은 [GitHub Releases](https://github.com/zensoku142/TokenMeter/releases)에서 확인하세요. +현재 버전: `1.11.4`. 변경 사항과 체크섬은 [GitHub Releases](https://github.com/zensoku142/TokenMeter/releases)에서 확인하세요. ## License diff --git a/README.md b/README.md index 5644817..bfe8f58 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ TokenMeter/ ## 版本与 Release -当前版本:`1.11.3`。更新记录及校验文件见 [GitHub Releases](https://github.com/zensoku142/TokenMeter/releases)。 +当前版本:`1.11.4`。更新记录及校验文件见 [GitHub Releases](https://github.com/zensoku142/TokenMeter/releases)。 2.0 的 Provider 能力边界、分批计划和已执行基础见 [TokenMeter 2.0 路线图](docs/V2_ROADMAP.md)。 diff --git a/README.zh-TW.md b/README.zh-TW.md index e597f57..a6877e3 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -129,7 +129,7 @@ TokenMeter/ ## 版本與 Release -目前版本:`1.11.3`。更新說明與校驗檔請見 [GitHub Releases](https://github.com/zensoku142/TokenMeter/releases)。 +目前版本:`1.11.4`。更新說明與校驗檔請見 [GitHub Releases](https://github.com/zensoku142/TokenMeter/releases)。 ## License diff --git a/api/providers/codex.py b/api/providers/codex.py index 64ec04c..2d13826 100644 --- a/api/providers/codex.py +++ b/api/providers/codex.py @@ -314,7 +314,7 @@ def _local_activity(self) -> tuple[tuple[tuple[str, int], ...], tuple[QuotaMetri statistics = ( QuotaMetric("累计 Token 数", self._compact_tokens(total_tokens)), QuotaMetric("峰值 Token 数", self._compact_tokens(peak_tokens)), - QuotaMetric("最长聊天时长", self._duration_text(longest_seconds)), + QuotaMetric("最长任务时长", self._duration_text(longest_seconds)), QuotaMetric("当前连续天数", f"{current_streak} 天"), QuotaMetric("最长连续天数", f"{longest_streak} 天"), ) diff --git a/app_identity.py b/app_identity.py index 69907cf..10fb51f 100644 --- a/app_identity.py +++ b/app_identity.py @@ -4,7 +4,7 @@ APP_DISPLAY_NAME = "TokenMeter" APP_STORAGE_NAME = "TokenSpider" -APP_VERSION = "1.11.3" +APP_VERSION = "1.11.4" # Keep the legacy storage and mutex identities so upgrades retain user data, # credentials, and single-instance coordination across every public rename. diff --git a/installer/TokenMeter.iss b/installer/TokenMeter.iss index 2feb8ca..bf4509c 100644 --- a/installer/TokenMeter.iss +++ b/installer/TokenMeter.iss @@ -1,5 +1,5 @@ #ifndef MyAppVersion - #define MyAppVersion "1.11.3" + #define MyAppVersion "1.11.4" #endif #define MyAppName "TokenMeter" #define MyAppExeName "TokenMeter.exe" diff --git a/release-notes/v1.11.4.md b/release-notes/v1.11.4.md new file mode 100644 index 0000000..ff49ef8 --- /dev/null +++ b/release-notes/v1.11.4.md @@ -0,0 +1,29 @@ +更新类型:功能优化与 Bug 修复 + +### 优化 + +- Codex 订阅摘要不再展示 `GPT-5.3-Codex-Spark` 额度,保留主要周额度、订阅套餐和套餐到期信息。 +- 套餐到期时间仅保留月日,减少面板中的冗余信息。 +- 重新设计 Provider 下拉框、选中态和高分辨率箭头,使浅色、深色主题与面板风格保持一致。 +- “最长聊天时长”调整为“最长任务时长”,统计口径保持不变。 + +### 修复 + +- 修复连续点击 Provider 下拉框时可能拖动主面板的问题。 +- 修复下拉菜单展开过程中出现黑色边角或系统阴影的问题,保持圆角边界完整。 + +### 兼容 + +- 不改变 Codex、DeepSeek 和 Xiaomi MiMo 的登录、额度查询及数据存储流程。 +- 不改变 Provider 返回结构、SQLite 历史数据、配置字段、凭据存储和自动更新协议。 + +### 验证 + +- 完整自动化测试通过。 +- Python 编译检查与 `git diff --check` 通过。 +- 已完成 Provider 下拉框交互、圆角边界和浅色/深色主题的本地界面验证。 + +### 安装与校验 + +- 从本 Release 下载 `TokenMeter-Setup-v1.11.4-x64.exe`。 +- 可使用同页 `SHA256SUMS.txt` 校验安装包完整性;SHA256 不等同于独立发布签名。 diff --git a/tests/test_providers.py b/tests/test_providers.py index 312873f..07d9e32 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -177,7 +177,7 @@ def token_event(total, cached=0): self.assertEqual([item.title for item in statistics], [ "累计 Token 数", "峰值 Token 数", - "最长聊天时长", + "最长任务时长", "当前连续天数", "最长连续天数", ]) diff --git a/tests/test_qt_ui.py b/tests/test_qt_ui.py index fdf20f8..60acb0d 100644 --- a/tests/test_qt_ui.py +++ b/tests/test_qt_ui.py @@ -2,7 +2,7 @@ from datetime import date, datetime, timedelta, timezone from decimal import Decimal from pathlib import Path -from unittest.mock import patch +from unittest.mock import Mock, patch os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") os.environ["APPDATA"] = str(Path.cwd() / ".test-appdata") @@ -15,6 +15,7 @@ from PySide6.QtWidgets import ( QApplication, QDialog, + QFrame, QLabel, QLineEdit, QPushButton, @@ -198,7 +199,7 @@ def test_panel_quick_switches_provider_and_renders_subscription_quota(): data.quota_statistics = [ QuotaMetric("累计 Token 数", "21.7亿"), QuotaMetric("峰值 Token 数", "1.1亿"), - QuotaMetric("最长聊天时长", "52分 35秒"), + QuotaMetric("最长任务时长", "52分 35秒"), QuotaMetric("当前连续天数", "0 天"), QuotaMetric("最长连续天数", "27 天"), ] @@ -225,9 +226,21 @@ def test_panel_quick_switches_provider_and_renders_subscription_quota(): assert panel.provider_quick_combo.view().objectName() == "headerProviderView" assert panel.provider_quick_combo.view().minimumWidth() == 132 panel.show() + combo_animation_enabled = APP.isEffectEnabled(Qt.UIEffect.UI_AnimateCombo) panel.provider_quick_combo.showPopup() APP.processEvents() - assert panel.provider_quick_combo.view().window().size() == QSize(132, 134) + popup = panel.provider_quick_combo.view().window() + assert popup.size() == QSize(132, 140) + assert popup.minimumSize() == QSize(132, 140) + assert popup.maximumSize() == QSize(132, 140) + assert popup.windowFlags() & Qt.WindowType.FramelessWindowHint + assert popup.windowFlags() & Qt.WindowType.NoDropShadowWindowHint + assert popup.testAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + assert popup.frameShape() == QFrame.Shape.NoFrame + assert panel.provider_quick_combo.view().frameShape() == QFrame.Shape.NoFrame + assert not popup.mask().contains(QPoint(0, 0)) + assert popup.mask().contains(popup.rect().center()) + assert APP.isEffectEnabled(Qt.UIEffect.UI_AnimateCombo) == combo_animation_enabled panel.provider_quick_combo.hidePopup() assert panel.today_card.title_label.text() == "每周额度" assert panel.today_card.value.text() == "已用 25%" @@ -288,12 +301,96 @@ def test_subscription_expiry_replaces_codex_placeholder_without_email(): assert panel.balance_card.detail.isHidden() assert panel.month_card.title_label.text() == "套餐到期" assert panel.month_card.value.text() == local_until.strftime("%m-%d") - assert panel.month_card.detail.text() == local_until.strftime("%Y") + assert panel.month_card.detail.isHidden() assert "a@example.com" not in panel.balance_card.value.toolTip() assert "a@example.com" not in panel.month_card.value.toolTip() panel.close() +def test_header_does_not_start_drag_from_provider_selector_events(): + panel = MainPanel() + panel.show() + APP.processEvents() + pressed: list[QPoint] = [] + dragged: list[QPoint] = [] + released: list[QPoint] = [] + panel.header.pressed.connect(pressed.append) + panel.header.dragged.connect(dragged.append) + panel.header.released.connect(released.append) + + combo_point = panel.provider_quick_combo.mapTo( + panel.header, panel.provider_quick_combo.rect().center() + ) + + def mouse_event(event_point: QPoint, *, pressed_button: bool) -> Mock: + event = Mock() + event.button.return_value = ( + Qt.MouseButton.LeftButton if pressed_button else Qt.MouseButton.NoButton + ) + event.buttons.return_value = ( + Qt.MouseButton.LeftButton if pressed_button else Qt.MouseButton.NoButton + ) + event.position.return_value = QPointF(event_point) + event.globalPosition.return_value = QPointF(panel.header.mapToGlobal(event_point)) + return event + + panel.header.mousePressEvent(mouse_event(combo_point, pressed_button=True)) + panel.header.mouseMoveEvent( + mouse_event(combo_point + QPoint(12, 0), pressed_button=True) + ) + panel.header.mouseReleaseEvent(mouse_event(combo_point, pressed_button=True)) + + assert pressed == [] + assert dragged == [] + assert released == [] + + free_point = QPoint(430, panel.header.height() // 2) + panel.header.mousePressEvent(mouse_event(free_point, pressed_button=True)) + panel.header.mouseMoveEvent( + mouse_event(free_point + QPoint(12, 0), pressed_button=True) + ) + panel.header.mouseReleaseEvent(mouse_event(free_point, pressed_button=True)) + + assert len(pressed) == 1 + assert len(dragged) == 1 + assert len(released) == 1 + panel.close() + + +def test_codex_spark_quota_is_hidden_without_hiding_other_subscription_details(): + panel = MainPanel() + data = sample_data() + active_until = datetime(2026, 8, 11, 6, 17, tzinfo=timezone.utc) + data.quota_windows = [ + QuotaWindow("codex-primary", "每周额度", 7), + QuotaWindow("codex-extra-0-primary", "GPT-5.3-Codex-Spark", 0), + ] + data.account_plan = "prolite" + data.account_plan_active_until = active_until + data.per_provider = [ + PerProviderData( + "codex", + "Codex", + quota_windows=list(data.quota_windows), + account_plan="prolite", + account_plan_active_until=active_until, + ) + ] + + panel.update_data(data) + + assert panel.today_card.title_label.text() == "每周额度" + assert panel.balance_card.title_label.text() == "订阅套餐" + assert panel.balance_card.value.text() == "prolite" + assert panel.month_card.title_label.text() == "套餐到期" + assert panel.month_card.value.text() == active_until.astimezone().strftime("%m-%d") + assert all( + "Spark" not in card.title_label.text() + for card in (panel.today_card, panel.balance_card, panel.month_card) + ) + panel.close() + + def test_reset_countdown_is_timezone_safe_and_readable(): now = datetime(2026, 8, 9, 8, 0, tzinfo=timezone.utc) assert format_reset_countdown(now + timedelta(days=1, hours=2), now) == "1 天 2 小时后重置" diff --git a/ui/qt_panel.py b/ui/qt_panel.py index b202cc4..72edb77 100644 --- a/ui/qt_panel.py +++ b/ui/qt_panel.py @@ -7,7 +7,18 @@ from math import ceil, floor import pyqtgraph as pg -from PySide6.QtCore import QDate, QLocale, QPoint, QRect, QRectF, QSignalBlocker, QSize, Qt, Signal +from PySide6.QtCore import ( + QDate, + QLocale, + QPoint, + QPointF, + QRect, + QRectF, + QSignalBlocker, + QSize, + Qt, + Signal, +) from PySide6.QtGui import ( QColor, QFont, @@ -17,10 +28,12 @@ QPainter, QPen, QPixmap, + QRegion, QShortcut, QTextCharFormat, ) from PySide6.QtWidgets import ( + QApplication, QCalendarWidget, QButtonGroup, QComboBox, @@ -112,11 +125,16 @@ def format_reset_countdown(value: datetime | None, now: datetime | None = None) return f"{minutes} 分钟后重置" -def format_plan_active_until(value: datetime | None) -> tuple[str, str]: +def format_plan_active_until(value: datetime | None) -> str: if value is None: - return "--", "" + return "--" local_value = value.astimezone() if value.tzinfo is not None else value - return local_value.strftime("%m-%d"), local_value.strftime("%Y") + return local_value.strftime("%m-%d") + + +def is_codex_spark_quota(title: str) -> bool: + normalized = "".join(str(title or "").casefold().split()) + return "codex-spark" in normalized class MoneyAxis(pg.AxisItem): @@ -143,20 +161,48 @@ class DraggableHeader(QFrame): dragged = Signal(QPoint) released = Signal(QPoint) + def __init__(self, parent: QWidget | None = None): + super().__init__(parent) + self._drag_active = False + + def _blocks_drag_at(self, point: QPoint) -> bool: + child = self.childAt(point) + while child is not None and child is not self: + if isinstance(child, (QComboBox, QToolButton)): + return True + child = child.parentWidget() + return False + def mousePressEvent(self, event) -> None: if event.button() == Qt.MouseButton.LeftButton: + if self._blocks_drag_at(event.position().toPoint()): + self._drag_active = False + event.accept() + return + self._drag_active = True self.pressed.emit(event.globalPosition().toPoint()) event.accept() + return + super().mousePressEvent(event) def mouseMoveEvent(self, event) -> None: if event.buttons() & Qt.MouseButton.LeftButton: + if not self._drag_active: + event.accept() + return self.dragged.emit(event.globalPosition().toPoint()) event.accept() + return + super().mouseMoveEvent(event) def mouseReleaseEvent(self, event) -> None: if event.button() == Qt.MouseButton.LeftButton: - self.released.emit(event.globalPosition().toPoint()) + if self._drag_active: + self.released.emit(event.globalPosition().toPoint()) + self._drag_active = False event.accept() + return + super().mouseReleaseEvent(event) class MinuteCalendarWidget(QCalendarWidget): @@ -495,7 +541,7 @@ def closeEvent(self, event) -> None: class ProviderOptionDelegate(QStyledItemDelegate): """Paint the header provider menu without changing its combo-box data model.""" - ROW_HEIGHT = 36 + ROW_HEIGHT = 34 def __init__(self, combo: QComboBox): super().__init__(combo) @@ -509,7 +555,7 @@ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index) -> None: tokens = current_theme() is_current = index.row() == self._combo.currentIndex() is_hovered = bool(option.state & QStyle.StateFlag.State_MouseOver) - row_rect = QRectF(option.rect.adjusted(4, 2, -4, -2)) + row_rect = QRectF(option.rect.adjusted(3, 2, -3, -2)) painter.save() painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) @@ -517,23 +563,17 @@ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index) -> None: painter.setPen(Qt.PenStyle.NoPen) if is_current: painter.setBrush(QColor(tokens.accent_soft)) - painter.drawRoundedRect(row_rect, 6, 6) - painter.setBrush(QColor(tokens.accent)) - painter.drawRoundedRect( - QRectF(row_rect.left(), row_rect.top() + 4, 3, row_rect.height() - 8), - 1.5, - 1.5, - ) + painter.drawRoundedRect(row_rect, 8, 8) elif is_hovered: hover = QColor(tokens.text) - hover.setAlpha(18) + hover.setAlpha(14) painter.setBrush(hover) - painter.drawRoundedRect(row_rect, 6, 6) + painter.drawRoundedRect(row_rect, 8, 8) painter.setPen(QColor(tokens.value if is_current else tokens.text)) painter.setFont(option.font) painter.drawText( - option.rect.adjusted(16, 0, -36, 0), + option.rect.adjusted(13, 0, -34, 0), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, str(index.data(Qt.ItemDataRole.DisplayRole) or ""), ) @@ -557,9 +597,10 @@ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index) -> None: class ProviderQuickCombo(QComboBox): - """Header-only provider selector with a compact Fluent visual treatment.""" + """Header provider selector styled as a lightweight title-bar pill.""" POPUP_WIDTH = 132 + POPUP_RADIUS = 10 def __init__(self, parent: QWidget | None = None): super().__init__(parent) @@ -570,8 +611,56 @@ def __init__(self, parent: QWidget | None = None): popup_view.setObjectName("headerProviderView") popup_view.setMouseTracking(True) popup_view.setMinimumWidth(self.POPUP_WIDTH) + popup_view.setFrameShape(QFrame.Shape.NoFrame) + popup_view.setLineWidth(0) + popup_view.setMidLineWidth(0) popup_view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) popup_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + popup = popup_view.window() + popup.setObjectName("headerProviderPopup") + popup.setWindowFlag(Qt.WindowType.FramelessWindowHint, True) + popup.setWindowFlag(Qt.WindowType.NoDropShadowWindowHint, True) + popup.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True) + popup.setAutoFillBackground(False) + self._remove_popup_frame(popup) + + @staticmethod + def _remove_popup_frame(popup: QWidget) -> None: + if isinstance(popup, QFrame): + popup.setFrameShape(QFrame.Shape.NoFrame) + popup.setLineWidth(0) + popup.setMidLineWidth(0) + popup.setContentsMargins(0, 0, 0, 0) + + @classmethod + def _rounded_popup_region(cls, rect: QRect) -> QRegion: + radius = cls.POPUP_RADIUS + diameter = radius * 2 + region = QRegion(radius, 0, rect.width() - diameter, rect.height()) + region += QRegion(0, radius, rect.width(), rect.height() - diameter) + region += QRegion(0, 0, diameter, diameter, QRegion.RegionType.Ellipse) + region += QRegion( + rect.width() - diameter, + 0, + diameter, + diameter, + QRegion.RegionType.Ellipse, + ) + region += QRegion( + 0, + rect.height() - diameter, + diameter, + diameter, + QRegion.RegionType.Ellipse, + ) + region += QRegion( + rect.width() - diameter, + rect.height() - diameter, + diameter, + diameter, + QRegion.RegionType.Ellipse, + ) + return region def paintEvent(self, _event) -> None: tokens = current_theme() @@ -580,45 +669,61 @@ def paintEvent(self, _event) -> None: painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True) control_rect = QRectF(self.rect()).adjusted(0.5, 0.5, -0.5, -0.5) if not self.isEnabled(): - border = tokens.disabled text = tokens.disabled + background = QColor(Qt.GlobalColor.transparent) elif self._popup_open or self.hasFocus(): - border = tokens.accent text = tokens.value + background = QColor(tokens.accent_soft) elif self.underMouse(): - border = tokens.border_hover text = tokens.value + background = QColor(tokens.elevated) else: - border = tokens.border text = tokens.value + background = QColor(Qt.GlobalColor.transparent) - painter.setPen(QPen(QColor(border), 1)) - painter.setBrush(QColor(tokens.surface)) + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(background) painter.drawRoundedRect(control_rect, 9, 9) painter.setPen(QColor(text)) painter.setFont(self.font()) painter.drawText( - self.rect().adjusted(12, 0, -32, 0), + self.rect().adjusted(11, 0, -34, 0), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self.currentText(), ) - arrow = fluent_icon("chevron-down", size=12, active_color=tokens.accent) - if not arrow.isNull(): - mode = QIcon.Mode.Active if self._popup_open or self.hasFocus() else QIcon.Mode.Normal - pixmap = arrow.pixmap(QSize(12, 12), mode, QIcon.State.Off) - painter.drawPixmap(QRect(self.width() - 23, 8, 12, 12), pixmap) + arrow_center = QPointF(self.width() - 18, self.height() / 2) + arrow_color = QColor( + tokens.accent + if self._popup_open or self.hasFocus() + else tokens.subtext + ) + arrow_pen = QPen(arrow_color) + arrow_pen.setWidthF(1.6) + arrow_pen.setCapStyle(Qt.PenCapStyle.RoundCap) + arrow_pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin) + painter.setPen(arrow_pen) + direction = -1 if self._popup_open else 1 + painter.drawLine( + QPointF(arrow_center.x() - 4, arrow_center.y() - 2 * direction), + QPointF(arrow_center.x(), arrow_center.y() + 2 * direction), + ) + painter.drawLine( + QPointF(arrow_center.x(), arrow_center.y() + 2 * direction), + QPointF(arrow_center.x() + 4, arrow_center.y() - 2 * direction), + ) painter.end() def showPopup(self) -> None: self._popup_open = True self.update() self.view().setMinimumWidth(self.POPUP_WIDTH) - super().showPopup() popup = self.view().window() + self._remove_popup_frame(popup) visible_rows = min(self.count(), self.maxVisibleItems()) - popup_height = visible_rows * ProviderOptionDelegate.ROW_HEIGHT + 26 - popup.resize(self.POPUP_WIDTH, popup_height) + popup_height = visible_rows * ProviderOptionDelegate.ROW_HEIGHT + 38 + popup.setFixedSize(self.POPUP_WIDTH, popup_height) + popup.setMask(self._rounded_popup_region(popup.rect())) below = self.mapToGlobal(QPoint(0, self.height() + 4)) above = self.mapToGlobal(QPoint(0, -popup_height - 4)) screen = QGuiApplication.screenAt(below) or self.screen() @@ -627,7 +732,27 @@ def showPopup(self) -> None: y = below.y() if y + popup.height() - 1 > available.bottom() and above.y() >= available.top(): y = above.y() - popup.move(x, max(available.top(), y)) + popup_position = QPoint(x, max(available.top(), y)) + popup.move(popup_position) + + # QComboBox normally shows its private container before callers can + # style and resize it. Prepare the final frame, mask, size and position + # first so Windows never composites the unrounded intermediate frame. + app = QApplication.instance() + combo_animation = Qt.UIEffect.UI_AnimateCombo + animation_enabled = bool(app and app.isEffectEnabled(combo_animation)) + if animation_enabled: + app.setEffectEnabled(combo_animation, False) + try: + super().showPopup() + finally: + if animation_enabled: + app.setEffectEnabled(combo_animation, True) + popup = self.view().window() + self._remove_popup_frame(popup) + popup.setFixedSize(self.POPUP_WIDTH, popup_height) + popup.setMask(self._rounded_popup_region(popup.rect())) + popup.move(popup_position) popup.raise_() def hidePopup(self) -> None: @@ -2635,7 +2760,12 @@ def update_data( if quota_mode: cards = (self.today_card, self.balance_card, self.month_card) summaries: list[tuple[str, str, str]] = [] - for window in data.quota_windows[:3]: + visible_windows = [ + window + for window in data.quota_windows + if not (provider_id == "codex" and is_codex_spark_quota(window.title)) + ] + for window in visible_windows[:3]: detail_parts = [ f"剩余 {100 - window.used_percent:.0f}%", format_reset_countdown(window.resets_at), @@ -2658,14 +2788,12 @@ def update_data( if len(summaries) < len(cards) and ( data.account_plan or data.account_plan_active_until ): - expiry_date, expiry_year = format_plan_active_until( - data.account_plan_active_until - ) + expiry_date = format_plan_active_until(data.account_plan_active_until) summaries.append( ( "套餐到期", "--" if loading else expiry_date, - "" if loading else expiry_year, + "", ) ) while len(summaries) < len(cards): diff --git a/ui/qt_theme.py b/ui/qt_theme.py index a6a2b9f..48b7ab5 100644 --- a/ui/qt_theme.py +++ b/ui/qt_theme.py @@ -218,12 +218,16 @@ def build_app_style(theme: ThemeTokens | str | None = None) -> str: padding: 0; font-size: 12px; }} +QFrame#headerProviderPopup {{ + background: transparent; + border: 0; +}} QAbstractItemView#headerProviderView {{ color: {tokens.text}; background: {tokens.elevated}; - border: 1px solid {tokens.border}; - border-radius: 8px; - padding: 4px; + border: 0; + border-radius: 10px; + padding: 5px; outline: 0; selection-background-color: transparent; selection-color: {tokens.text}; diff --git a/updater_version_info.txt b/updater_version_info.txt index 20669c1..fbfdc84 100644 --- a/updater_version_info.txt +++ b/updater_version_info.txt @@ -15,11 +15,11 @@ VSVersionInfo( '040904B0', [ StringStruct('FileDescription', 'TokenMeter standalone updater'), - StringStruct('FileVersion', '1.11.3'), + StringStruct('FileVersion', '1.11.4'), StringStruct('InternalName', 'TokenMeterUpdater'), StringStruct('OriginalFilename', 'TokenMeterUpdater.exe'), StringStruct('ProductName', 'TokenMeterUpdater'), - StringStruct('ProductVersion', '1.11.3') + StringStruct('ProductVersion', '1.11.4') ] ) ]), diff --git a/version_info.txt b/version_info.txt index 20ce67a..dac9947 100644 --- a/version_info.txt +++ b/version_info.txt @@ -15,11 +15,11 @@ VSVersionInfo( '040904B0', [ StringStruct('FileDescription', 'TokenMeter API usage monitor'), - StringStruct('FileVersion', '1.11.3'), + StringStruct('FileVersion', '1.11.4'), StringStruct('InternalName', 'TokenMeter'), StringStruct('OriginalFilename', 'TokenMeter.exe'), StringStruct('ProductName', 'TokenMeter'), - StringStruct('ProductVersion', '1.11.3') + StringStruct('ProductVersion', '1.11.4') ] ) ]), From 87c6b7f748cb2b137a71af98ac2c9197259c5801 Mon Sep 17 00:00:00 2001 From: superadminist <18433706+superadminist@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:33:49 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=E5=85=BC=E5=AE=B9=20Qt=20=E6=97=A0?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E5=BC=B9=E7=AA=97=E6=A0=87=E5=BF=97=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_qt_ui.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_qt_ui.py b/tests/test_qt_ui.py index 60acb0d..5553101 100644 --- a/tests/test_qt_ui.py +++ b/tests/test_qt_ui.py @@ -234,7 +234,6 @@ def test_panel_quick_switches_provider_and_renders_subscription_quota(): assert popup.minimumSize() == QSize(132, 140) assert popup.maximumSize() == QSize(132, 140) assert popup.windowFlags() & Qt.WindowType.FramelessWindowHint - assert popup.windowFlags() & Qt.WindowType.NoDropShadowWindowHint assert popup.testAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) assert popup.frameShape() == QFrame.Shape.NoFrame assert panel.provider_quick_combo.view().frameShape() == QFrame.Shape.NoFrame