From d107b7ce7b34bdad0fa660604f0d26d063e68fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AI=E4=BA=A7=E5=93=81=E9=BB=84=E5=8F=94?= Date: Thu, 27 Aug 2026 21:44:54 +0800 Subject: [PATCH] release: keep Trojan Inferno renderer mounted to stop popover flash in 0.2.11 --- README.md | 16 +++--- .../OdysseyMotionPrototype.swift | 45 ++++++++++++---- .../OdysseyMotionPrototypeTests.swift | 52 ++++++++++++++----- docs/RELEASE_NOTES_0.2.11.md | 11 ++++ script/build_swiftui_and_run.sh | 2 +- script/package_release.sh | 4 +- 6 files changed, 99 insertions(+), 31 deletions(-) create mode 100644 docs/RELEASE_NOTES_0.2.11.md diff --git a/README.md b/README.md index 0b4997c..b491b6f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ TokenStep 是一个 macOS 菜单栏 App,用来本地统计你在 Codex、Claud 下载最新版 DMG,打开后把 `TokenStep.app` 拖进「应用程序」即可使用: -[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.10.dmg) +[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.11.dmg) 也可以从 Release 页面查看所有版本: @@ -32,6 +32,10 @@ TokenStep 已使用 Developer ID 签名并通过 Apple 公证。首次打开时 Windows版本由十七做了移植,欢迎大家前往使用:https://github.com/canyexuanfan/TokenStep-Windows/releases +## 0.2.11 更新:修复特洛伊火海浮层闪烁 + +0.2.11 修复了奥德赛「特洛伊火海」篇章打开菜单栏浮层时偶发的一帧闪烁。动态渲染层现在常驻并随窗口可见性暂停、恢复,不再在点击浮层后临时插入;截图稳定性、省电暂停以及其他主题行为保持不变。完整说明见 [0.2.11 发布说明](docs/RELEASE_NOTES_0.2.11.md)。 + ## 0.2.10 更新:坠入黑洞的运镜方向 0.2.10 修正了坠落模式的逼近方向:膨胀锚点从画面中部移到**黑洞本体**(左上视界阴影处),让黑洞成为整个画面的放射原点——所有元素从它向外流走,右侧弧线不断放大、冲出画面右缘,弧顶顺流右移,读起来就是"视角正在被吸进去",而不是画面整体向上顶。 @@ -138,7 +142,7 @@ TokenStep 第一次从“更换配色”升级为完整的**主题皮肤包系 - 新增奥德赛弓箭阶梯 Logo;用量采用骨金、冷金或余烬橙,绿色只保留给同步成功等状态反馈。 - 关闭排行榜后浮层会自动收短,多来源额度则使用紧凑布局完整展示。 -打开 `设置 → 通用 → 主题皮肤包` 即可切换。完整说明见 [0.2.4 发布说明](docs/RELEASE_NOTES_0.2.4.md),或直接[下载已签名并通过 Apple 公证的最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.10.dmg)。 +打开 `设置 → 通用 → 主题皮肤包` 即可切换。完整说明见 [0.2.4 发布说明](docs/RELEASE_NOTES_0.2.4.md),或直接[下载已签名并通过 Apple 公证的最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.11.dmg)。 ## TokenStep 适合谁? @@ -197,7 +201,7 @@ TokenStep 默认只做本地统计。 ## 安装方式 -1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.10.dmg)。 +1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.11.dmg)。 2. 打开 DMG。 3. 把 `TokenStep.app` 拖到「应用程序」。 4. 启动 TokenStep。 @@ -257,7 +261,7 @@ TokenStepSwift/dist/TokenStep.app Developer ID 签名: ```bash -TOKENSTEP_VERSION=0.2.10 \ +TOKENSTEP_VERSION=0.2.11 \ CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \ ./script/package_release.sh ``` @@ -265,7 +269,7 @@ CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \ 签名 + Apple 公证: ```bash -TOKENSTEP_VERSION=0.2.10 \ +TOKENSTEP_VERSION=0.2.11 \ CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \ TOKENSTEP_NOTARY_PROFILE="tokenstep-notary" \ ./script/package_release.sh --notarize @@ -280,7 +284,7 @@ release/TokenStep-.dmg 维护者说明见 [docs/RELEASE.md](docs/RELEASE.md)。 -0.2.10 发布说明见 [docs/RELEASE_NOTES_0.2.10.md](docs/RELEASE_NOTES_0.2.10.md);0.2.9 发布说明见 [docs/RELEASE_NOTES_0.2.9.md](docs/RELEASE_NOTES_0.2.9.md);0.2.8 发布说明见 [docs/RELEASE_NOTES_0.2.8.md](docs/RELEASE_NOTES_0.2.8.md);0.2.7 发布说明见 [docs/RELEASE_NOTES_0.2.7.md](docs/RELEASE_NOTES_0.2.7.md);引力边界实现说明见 [docs/INTERSTELLAR_THEME_PACK_0.2.7.md](docs/INTERSTELLAR_THEME_PACK_0.2.7.md);0.2.6 更新闭环说明见 [docs/RELEASE_NOTES_0.2.6.md](docs/RELEASE_NOTES_0.2.6.md);0.2.4 奥德赛主题包说明见 [docs/ODYSSEY_THEME_PACK_0.2.4.md](docs/ODYSSEY_THEME_PACK_0.2.4.md)。 +0.2.11 发布说明见 [docs/RELEASE_NOTES_0.2.11.md](docs/RELEASE_NOTES_0.2.11.md);0.2.10 发布说明见 [docs/RELEASE_NOTES_0.2.10.md](docs/RELEASE_NOTES_0.2.10.md);0.2.9 发布说明见 [docs/RELEASE_NOTES_0.2.9.md](docs/RELEASE_NOTES_0.2.9.md);0.2.8 发布说明见 [docs/RELEASE_NOTES_0.2.8.md](docs/RELEASE_NOTES_0.2.8.md);0.2.7 发布说明见 [docs/RELEASE_NOTES_0.2.7.md](docs/RELEASE_NOTES_0.2.7.md);引力边界实现说明见 [docs/INTERSTELLAR_THEME_PACK_0.2.7.md](docs/INTERSTELLAR_THEME_PACK_0.2.7.md);0.2.6 更新闭环说明见 [docs/RELEASE_NOTES_0.2.6.md](docs/RELEASE_NOTES_0.2.6.md);0.2.4 奥德赛主题包说明见 [docs/ODYSSEY_THEME_PACK_0.2.4.md](docs/ODYSSEY_THEME_PACK_0.2.4.md)。 ## 开源协议 diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/OdysseyMotion/OdysseyMotionPrototype.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/OdysseyMotion/OdysseyMotionPrototype.swift index d47670c..63ba05c 100644 --- a/TokenStepSwift/Sources/TokenStepSwift/Views/OdysseyMotion/OdysseyMotionPrototype.swift +++ b/TokenStepSwift/Sources/TokenStepSwift/Views/OdysseyMotion/OdysseyMotionPrototype.swift @@ -35,19 +35,32 @@ enum OdysseyMotionPrototypeMode: String, CaseIterable { } enum OdysseyMotionPrototypeEligibility { - static func shouldRender( + static func shouldMount( isVoyage: Bool, chapter: TokenStepOdysseyChapter, mode: OdysseyMotionPrototypeMode, - isSurfaceVisible: Bool, isScreenshotRendering: Bool ) -> Bool { isVoyage && chapter == .trojanInferno && mode != .off - && isSurfaceVisible && !isScreenshotRendering } + + static func shouldAnimate( + isVoyage: Bool, + chapter: TokenStepOdysseyChapter, + mode: OdysseyMotionPrototypeMode, + isSurfaceVisible: Bool, + isScreenshotRendering: Bool + ) -> Bool { + shouldMount( + isVoyage: isVoyage, + chapter: chapter, + mode: mode, + isScreenshotRendering: isScreenshotRendering + ) && isSurfaceVisible + } } struct OdysseyTrojanAmbientView: View { @@ -57,8 +70,17 @@ struct OdysseyTrojanAmbientView: View { @StateObject private var controller = OdysseyTrojanSceneController() - private var shouldRender: Bool { - OdysseyMotionPrototypeEligibility.shouldRender( + private var shouldMount: Bool { + OdysseyMotionPrototypeEligibility.shouldMount( + isVoyage: TokenStepThemeRuntime.isVoyage, + chapter: TokenStepThemeRuntime.odysseyChapter, + mode: mode, + isScreenshotRendering: isScreenshotRendering + ) + } + + private var shouldAnimate: Bool { + OdysseyMotionPrototypeEligibility.shouldAnimate( isVoyage: TokenStepThemeRuntime.isVoyage, chapter: TokenStepThemeRuntime.odysseyChapter, mode: mode, @@ -69,28 +91,31 @@ struct OdysseyTrojanAmbientView: View { var body: some View { GeometryReader { proxy in - if shouldRender { + if shouldMount { SpriteView( scene: controller.scene, + // Keep the hosting SKView alive and rendering. Visibility only + // pauses the SKScene below; pausing both can leave a scene that + // was mounted while hidden without a resumable first frame. isPaused: false, preferredFramesPerSecond: mode.preferredFramesPerSecond, options: [.allowsTransparency, .shouldCullNonVisibleNodes] ) .onAppear { - controller.update(size: proxy.size, mode: mode, isActive: true) + controller.update(size: proxy.size, mode: mode, isActive: shouldAnimate) } .onChange(of: proxy.size) { _, newSize in - controller.update(size: newSize, mode: mode, isActive: true) + controller.update(size: newSize, mode: mode, isActive: shouldAnimate) } .onChange(of: mode) { _, newMode in - controller.update(size: proxy.size, mode: newMode, isActive: true) + controller.update(size: proxy.size, mode: newMode, isActive: shouldAnimate) } .onDisappear { controller.setRenderingActive(false) } } } - .onChange(of: shouldRender) { _, active in + .onChange(of: shouldAnimate) { _, active in controller.setRenderingActive(active) } .onDisappear { diff --git a/TokenStepSwift/Tests/TokenStepSwiftTests/OdysseyMotionPrototypeTests.swift b/TokenStepSwift/Tests/TokenStepSwiftTests/OdysseyMotionPrototypeTests.swift index c69237c..950b341 100644 --- a/TokenStepSwift/Tests/TokenStepSwiftTests/OdysseyMotionPrototypeTests.swift +++ b/TokenStepSwift/Tests/TokenStepSwiftTests/OdysseyMotionPrototypeTests.swift @@ -15,13 +15,15 @@ final class OdysseyMotionPrototypeTests: XCTestCase { ) } - func testEligibilityRequiresTrojanVisibleVoyageSurfaceAndSuppressesScreenshots() { - XCTAssertTrue(eligibility()) - XCTAssertFalse(eligibility(isVoyage: false)) - XCTAssertFalse(eligibility(chapter: .aegeanMist)) - XCTAssertFalse(eligibility(mode: .off)) - XCTAssertFalse(eligibility(isSurfaceVisible: false)) - XCTAssertFalse(eligibility(isScreenshotRendering: true)) + func testTrojanRendererStaysMountedWhileHiddenAndOnlyAnimatesWhenVisible() { + XCTAssertTrue(mountEligibility()) + XCTAssertTrue(animationEligibility()) + XCTAssertTrue(mountEligibility(isSurfaceVisible: false)) + XCTAssertFalse(animationEligibility(isSurfaceVisible: false)) + XCTAssertFalse(mountEligibility(isVoyage: false)) + XCTAssertFalse(mountEligibility(chapter: .aegeanMist)) + XCTAssertFalse(mountEligibility(mode: .off)) + XCTAssertFalse(mountEligibility(isScreenshotRendering: true)) } func testSceneKeepsConfiguredEmitterSetAcrossThirtyVisibilityCycles() async { @@ -78,20 +80,31 @@ final class OdysseyMotionPrototypeTests: XCTestCase { window.alphaValue = 0 coordinator.attach(to: window) - try await Task.sleep(nanoseconds: 50_000_000) + try await waitForVisibility(false, observedValues: { observedValues }) XCTAssertEqual(observedValues.last, false) window.orderFront(nil) - try await Task.sleep(nanoseconds: 80_000_000) + try await waitForVisibility(true, observedValues: { observedValues }) XCTAssertEqual(observedValues.last, true) window.orderOut(nil) - try await Task.sleep(nanoseconds: 80_000_000) + try await waitForVisibility(false, observedValues: { observedValues }) XCTAssertEqual(observedValues.last, false) coordinator.detach() } + @MainActor + private func waitForVisibility( + _ expected: Bool, + observedValues: () -> [Bool] + ) async throws { + for _ in 0..<25 { + if observedValues().last == expected { return } + try await Task.sleep(nanoseconds: 20_000_000) + } + } + func testDiagnosticsWritesTransitionLogOnlyWhenExplicitlyEnabled() throws { let directory = FileManager.default.temporaryDirectory .appendingPathComponent("tokenstep-odyssey-motion-diagnostics-\(UUID().uuidString)") @@ -126,14 +139,29 @@ final class OdysseyMotionPrototypeTests: XCTestCase { XCTAssertTrue(content.contains("uptime_ns=")) } - private func eligibility( + private func mountEligibility( + isVoyage: Bool = true, + chapter: TokenStepOdysseyChapter = .trojanInferno, + mode: OdysseyMotionPrototypeMode = .automatic, + isSurfaceVisible: Bool = true, + isScreenshotRendering: Bool = false + ) -> Bool { + OdysseyMotionPrototypeEligibility.shouldMount( + isVoyage: isVoyage, + chapter: chapter, + mode: mode, + isScreenshotRendering: isScreenshotRendering + ) + } + + private func animationEligibility( isVoyage: Bool = true, chapter: TokenStepOdysseyChapter = .trojanInferno, mode: OdysseyMotionPrototypeMode = .automatic, isSurfaceVisible: Bool = true, isScreenshotRendering: Bool = false ) -> Bool { - OdysseyMotionPrototypeEligibility.shouldRender( + OdysseyMotionPrototypeEligibility.shouldAnimate( isVoyage: isVoyage, chapter: chapter, mode: mode, diff --git a/docs/RELEASE_NOTES_0.2.11.md b/docs/RELEASE_NOTES_0.2.11.md new file mode 100644 index 0000000..3b73135 --- /dev/null +++ b/docs/RELEASE_NOTES_0.2.11.md @@ -0,0 +1,11 @@ +# TokenStep 0.2.11 + +TokenStep 0.2.11 fixes the one-frame flash that could appear when opening the menu-bar popover with Odyssey's Trojan Inferno chapter selected. + +## Fixed + +- Keep the Trojan SpriteKit renderer mounted while the popover is hidden, instead of inserting it after the window becomes visible. +- Pause and resume the existing scene with window visibility, preserving the previous background, screenshot, and energy-saving behavior. +- Add regression coverage that separates renderer mounting from animation eligibility. + +Token totals, costs, quotas, rankings, collectors, and all other theme chapters are unchanged. diff --git a/script/build_swiftui_and_run.sh b/script/build_swiftui_and_run.sh index fe1b455..2444af4 100755 --- a/script/build_swiftui_and_run.sh +++ b/script/build_swiftui_and_run.sh @@ -23,7 +23,7 @@ HELPER_EXECUTABLE="$BUILD_DIR/$HELPER_NAME" ICON_FILE="$ROOT_DIR/TokenUsageMenuApp/assets/TokenStepIcon.icns" ODYSSEY_ASSET_DIR="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey" INTERSTELLAR_ASSET_DIR="$ROOT_DIR/TokenUsageMenuApp/assets/interstellar" -VERSION="${TOKENSTEP_VERSION:-0.2.10}" +VERSION="${TOKENSTEP_VERSION:-0.2.11}" LAUNCH=true VERIFY=false diff --git a/script/package_release.sh b/script/package_release.sh index 6961ae4..fdc28de 100755 --- a/script/package_release.sh +++ b/script/package_release.sh @@ -8,14 +8,14 @@ SWIFT_DIR="$ROOT_DIR/TokenStepSwift" BUILT_APP_BUNDLE="$SWIFT_DIR/dist/$APP_NAME.app" APP_BUNDLE="$BUILT_APP_BUNDLE" RELEASE_DIR="$ROOT_DIR/release" -VERSION="${TOKENSTEP_VERSION:-0.2.8}" +VERSION="${TOKENSTEP_VERSION:-0.2.11}" IDENTITY="${CODE_SIGN_IDENTITY:-}" NOTARIZE=false usage() { cat <<'USAGE' Usage: - TOKENSTEP_VERSION=0.2.8 CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" ./script/package_release.sh [--notarize] + TOKENSTEP_VERSION=0.2.11 CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" ./script/package_release.sh [--notarize] Notarization credentials, choose one: TOKENSTEP_NOTARY_PROFILE="notarytool-profile"