Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 页面查看所有版本:

Expand All @@ -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 修正了坠落模式的逼近方向:膨胀锚点从画面中部移到**黑洞本体**(左上视界阴影处),让黑洞成为整个画面的放射原点——所有元素从它向外流走,右侧弧线不断放大、冲出画面右缘,弧顶顺流右移,读起来就是"视角正在被吸进去",而不是画面整体向上顶。
Expand Down Expand Up @@ -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 适合谁?

Expand Down Expand Up @@ -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。
Expand Down Expand Up @@ -257,15 +261,15 @@ 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
```

签名 + 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
Expand All @@ -280,7 +284,7 @@ release/TokenStep-<version>.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)。

## 开源协议

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)")
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions docs/RELEASE_NOTES_0.2.11.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion script/build_swiftui_and_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions script/package_release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading