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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@ jobs:
run: ./script/test_settings_codable.sh
- name: Run compact popover checks
run: ./script/test_compact_popover.sh
- name: Run Today overview checks
run: ./script/test_today_overview.sh
- name: Build TokenStep
run: ./script/build_swiftui_and_run.sh --no-launch
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

TokenStep 各版本的更新内容,新版本在前。每个版本的完整说明在 [docs/RELEASE_NOTES_<版本号>.md](docs/)。

## 0.2.21 更新:仪表盘与设置改版

仪表盘和设置参考 Token Rank 个人页重新设计:今日页新增与昨天对比、Token 构成、近 5 周热力图和按客户端堆叠的分时图;历史页新增年度热力图、常用时段和最长连续达标;设置改为左侧栏分组,改动立即生效。详见 [0.2.21 发布说明](docs/RELEASE_NOTES_0.2.21.md)。

## 0.2.20 更新:额度到点重置,面板信息保持最新

额度窗口过了重置时间显示「已重置」并自动重新读取;面板一直开着时,倒计时、时间进度竖线和提前用完的提示也会每分钟更新;修复刘海卡片关闭菜单后意外收起、鼠标指针停在手形的问题。详见 [0.2.20 发布说明](docs/RELEASE_NOTES_0.2.20.md)。
Expand Down
10 changes: 5 additions & 5 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.20.dmg)
[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.21.dmg)

也可以从 Release 页面查看所有版本:

Expand All @@ -32,9 +32,9 @@ TokenStep 已使用 Developer ID 签名并通过 Apple 公证。首次打开时

Windows版本由十七做了移植,欢迎大家前往使用:https://github.com/canyexuanfan/TokenStep-Windows/releases

## 最新版本:0.2.20
## 最新版本:0.2.21

额度窗口过了重置时间显示「已重置」并自动重新读取;面板一直开着时,倒计时、时间进度竖线和提前用完的提示也会每分钟更新;修复刘海卡片关闭菜单后意外收起、鼠标指针停在手形的问题。详见 [0.2.20 发布说明](docs/RELEASE_NOTES_0.2.20.md)。
仪表盘和设置参考 Token Rank 个人页重新设计:今日页新增与昨天对比、Token 构成、近 5 周热力图和按客户端堆叠的分时图;历史页新增年度热力图、常用时段和最长连续达标;设置改为左侧栏分组,改动立即生效。详见 [0.2.21 发布说明](docs/RELEASE_NOTES_0.2.21.md)。

历次更新(主题包、引力动效、自动更新等)见 [CHANGELOG.md](CHANGELOG.md)。

Expand Down Expand Up @@ -95,7 +95,7 @@ TokenStep 默认只做本地统计。

## 安装方式

1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.20.dmg)。
1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.21.dmg)。
2. 打开 DMG。
3. 把 `TokenStep.app` 拖到「应用程序」。
4. 启动 TokenStep。
Expand Down Expand Up @@ -166,7 +166,7 @@ TokenStepSwift/dist/TokenStep.app
公开发布强制执行 Developer ID 签名、Apple 公证、票据装订、系统分发检查和隔离安装验证。不再生成可被误上传的“仅签名、未公证”发布包:

```bash
TOKENSTEP_VERSION=0.2.20 \
TOKENSTEP_VERSION=0.2.21 \
CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
TOKENSTEP_NOTARY_PROFILE="tokenstep-notary" \
./script/package_release.sh --notarize
Expand Down
120 changes: 120 additions & 0 deletions TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,66 @@ enum TokenStepLocalization {

private static let translations: [TokenStepLanguage: [String: String]] = [
.en: [
"还没到": "Not yet",
"这一小时没有用量": "No usage this hour",
"改动立即生效,不需要保存。": "Changes apply right away. Nothing to save.",
"主题和配色,随时切换。": "Themes and colors. Switch any time.",
"只读取本机日志里的用量数字,不读对话内容。": "Reads only usage numbers from local logs, never conversations.",
"读取订阅的剩余额度,显示在面板顶部。密钥只存进钥匙串。": "Reads the quota left on your plans and shows it at the top of the panel. Keys stay in the Keychain.",
"在面板里显示你在 Token Rank 公开榜单上的排名。": "Shows your rank on the public Token Rank board in the panel.",
"只在本机统计,不上传用量": "Counts on this Mac only, uploads nothing",
"恢复默认设置": "Restore defaults",
"各自本机登录态": "Each tool's local login",
"Token Rank 排行榜": "Token Rank board",
"只读取公开榜单和本机 Token Rank 账号,TokenStep 不上传任何用量": "Reads only the public board and your local Token Rank account. TokenStep uploads no usage.",
"在面板里显示": "Show in the panel",
"等待关联": "Waiting to link",
"自 %@ 起": "Since %@",
"达标 %d 天": "%d goal days",
"最长连续达标": "Longest goal streak",
"当前连续 %d 天": "Current streak %d days",
"消耗热力图": "Usage heatmap",
"按天 · 描边为今天": "By day · outlined is today",
"%d月": "%d月",
"常用时段": "Busy hours",
"近 30 天": "Last 30 days",
"最常在 %02d:00–%02d:00 用 Agent": "Busiest %02d:00–%02d:00",
"日均 %@": "%@ a day",
"累计分布": "All-time split",
"每日明细": "Daily detail",
"导出 CSV": "Export CSV",
"展开全部 %d 天": "Show all %d days",
"主力客户端": "Top client",
"%@ 今天": "%@ today",
"未达标": "Below goal",
"%d 圈": "%d laps",
"今日已跑满 %d 圈": "%d laps done today",
"比昨天同一时间": "Versus this time yesterday",
"多 %@": "%@ more",
"少 %@": "%@ less",
"Token 构成": "Token mix",
"缓存命中 %@": "Cache hits %@",
"估算金额": "Estimated cost",
"按官方单价估算": "At official list prices",
"活跃时长": "Active time",
"有用量的整点数": "Hours with usage",
"工具调用 %@ 次": "%@ tool calls",
"共 %d 人": "of %d",
"累计 Token": "Total tokens",
"活跃 %d 天": "%d active days",
"近 5 周": "Last 5 weeks",
"全部历史 →": "All history →",
"连续达标 %d 天": "%d-day goal streak",
"一": "M",
"三": "W",
"五": "F",
"日": "S",
"今日分时消耗": "Today by hour",
"峰值 %02d:00 · %@": "Peak %02d:00 · %@",
"另有 %@ 没有时间戳,未计入分时": "%@ more has no timestamp and is not in the hourly chart",
"客户端": "Clients",
"模型排行": "Top models",
"今日 · 金额为估算": "Today · costs are estimates",
"自动": "Auto",
"刘海左侧": "Left of notch",
"刘海右侧": "Right of notch",
Expand Down Expand Up @@ -724,6 +784,66 @@ enum TokenStepLocalization {
"榜": "Rank",
],
.zhHant: [
"还没到": "還沒到",
"这一小时没有用量": "這一小時沒有用量",
"改动立即生效,不需要保存。": "改動立即生效,不需要儲存。",
"主题和配色,随时切换。": "主題和配色,隨時切換。",
"只读取本机日志里的用量数字,不读对话内容。": "只讀取本機日誌裡的用量數字,不讀對話內容。",
"读取订阅的剩余额度,显示在面板顶部。密钥只存进钥匙串。": "讀取訂閱的剩餘額度,顯示在面板頂部。密鑰只存進鑰匙圈。",
"在面板里显示你在 Token Rank 公开榜单上的排名。": "在面板裡顯示你在 Token Rank 公開榜單上的排名。",
"只在本机统计,不上传用量": "只在本機統計,不上傳用量",
"恢复默认设置": "恢復預設設定",
"各自本机登录态": "各自本機登入狀態",
"Token Rank 排行榜": "Token Rank 排行榜",
"只读取公开榜单和本机 Token Rank 账号,TokenStep 不上传任何用量": "只讀取公開榜單和本機 Token Rank 帳號,TokenStep 不上傳任何用量",
"在面板里显示": "在面板裡顯示",
"等待关联": "等待關聯",
"自 %@ 起": "自 %@ 起",
"达标 %d 天": "達標 %d 天",
"最长连续达标": "最長連續達標",
"当前连续 %d 天": "目前連續 %d 天",
"消耗热力图": "消耗熱力圖",
"按天 · 描边为今天": "按天 · 描邊為今天",
"%d月": "%d月",
"常用时段": "常用時段",
"近 30 天": "近 30 天",
"最常在 %02d:00–%02d:00 用 Agent": "最常在 %02d:00–%02d:00 用 Agent",
"日均 %@": "日均 %@",
"累计分布": "累計分佈",
"每日明细": "每日明細",
"导出 CSV": "匯出 CSV",
"展开全部 %d 天": "展開全部 %d 天",
"主力客户端": "主力客戶端",
"%@ 今天": "%@ 今天",
"未达标": "未達標",
"%d 圈": "%d 圈",
"今日已跑满 %d 圈": "今日已跑滿 %d 圈",
"比昨天同一时间": "比昨天同一時間",
"多 %@": "多 %@",
"少 %@": "少 %@",
"Token 构成": "Token 構成",
"缓存命中 %@": "快取命中 %@",
"估算金额": "估算金額",
"按官方单价估算": "按官方單價估算",
"活跃时长": "活躍時長",
"有用量的整点数": "有用量的整點數",
"工具调用 %@ 次": "工具呼叫 %@ 次",
"共 %d 人": "共 %d 人",
"累计 Token": "累計 Token",
"活跃 %d 天": "活躍 %d 天",
"近 5 周": "近 5 週",
"全部历史 →": "全部歷史 →",
"连续达标 %d 天": "連續達標 %d 天",
"一": "一",
"三": "三",
"五": "五",
"日": "日",
"今日分时消耗": "今日分時消耗",
"峰值 %02d:00 · %@": "峰值 %02d:00 · %@",
"另有 %@ 没有时间戳,未计入分时": "另有 %@ 沒有時間戳,未計入分時",
"客户端": "客戶端",
"模型排行": "模型排行",
"今日 · 金额为估算": "今日 · 金額為估算",
"经典": "經典",
"奥德赛": "奧德賽",
"星际": "星際",
Expand Down
116 changes: 15 additions & 101 deletions TokenStepSwift/Sources/TokenStepSwift/Views/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,16 @@ struct TokenCard<Content: View>: View {
self.content = content()
}

/// Close to the Token Rank profile cards: a small radius, a hairline and,
/// on the classic theme, only a faint shadow.
static var cornerRadius: CGFloat { 16 }

var body: some View {
let shape = RoundedRectangle(cornerRadius: 24, style: .continuous)
let shape = RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)

content
.padding(24)
.padding(20)
.frame(maxWidth: .infinity, alignment: .leading)
.background {
ZStack {
shape.fill(Color.tokenSurface)
Expand All @@ -286,13 +291,18 @@ struct TokenCard<Content: View>: View {
ZStack {
shape.stroke(Color.tokenHairline)
if TokenStepThemeRuntime.isVoyage {
VoyageCardOrnament(cornerRadius: 24)
VoyageCardOrnament(cornerRadius: Self.cornerRadius)
} else if TokenStepThemeRuntime.isInterstellar {
InterstellarCardOrnament(cornerRadius: 24)
InterstellarCardOrnament(cornerRadius: Self.cornerRadius)
}
}
}
.shadow(color: Color.tokenShadow, radius: 24, x: 0, y: 14)
.shadow(
color: TokenStepThemeRuntime.isCinematic ? Color.tokenShadow : Color.black.opacity(0.04),
radius: TokenStepThemeRuntime.isCinematic ? 24 : 2,
x: 0,
y: TokenStepThemeRuntime.isCinematic ? 14 : 1
)
}
}

Expand Down Expand Up @@ -619,102 +629,6 @@ struct TokenToolLegend: View {
}
}

struct ContributionWallView: View {
var rows: [DailyUsage]
var goal: Int
var weeks: Int = 34

private var rowByDate: [String: DailyUsage] {
Dictionary(uniqueKeysWithValues: rows.map { ($0.date, $0) })
}

var body: some View {
let calendar = Calendar(identifier: .gregorian)
let today = calendar.startOfDay(for: Date())
let rawStart = calendar.date(byAdding: .day, value: -(weeks * 7 - 1), to: today) ?? today
let weekday = calendar.component(.weekday, from: rawStart)
let mondayOffset = (weekday + 5) % 7
let start = calendar.date(byAdding: .day, value: -mondayOffset, to: rawStart) ?? rawStart

VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .top, spacing: 5) {
ForEach(0..<weeks, id: \.self) { week in
let firstDay = calendar.date(byAdding: .day, value: week * 7, to: start) ?? today
let showMonth = week == 0 || calendar.component(.month, from: firstDay) != calendar.component(.month, from: calendar.date(byAdding: .day, value: -7, to: firstDay) ?? firstDay)
VStack(spacing: 5) {
Text(showMonth ? monthLabel(firstDay) : " ")
.font(.system(size: 9, weight: .bold))
.foregroundStyle(.secondary)
.frame(width: 15, alignment: .leading)
.lineLimit(1)
ForEach(0..<7, id: \.self) { dayIndex in
let day = calendar.date(byAdding: .day, value: week * 7 + dayIndex, to: start) ?? today
let key = DateFormatter.tokenStepDay.string(from: day)
let usage = rowByDate[key]
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(day > today ? Color.clear : contributionColor(tokens: usage?.totalTokens ?? 0, goal: goal))
.frame(width: 15, height: 15)
.overlay {
if calendar.isDate(day, inSameDayAs: today) {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.stroke(Color.tokenGreenDark, lineWidth: 1.5)
}
}
.help(day > today ? "" : cellHelp(date: key, usage: usage))
}
}
}
}

HStack {
MetricPill(label: L("活跃"), value: localizedDays(rows.filter { $0.totalTokens > 0 }.count))
MetricPill(label: L("达标"), value: localizedDays(rows.filter { $0.totalTokens >= goal }.count))
MetricPill(label: L("最高"), value: TokenStepFormat.tokens(rows.map(\.totalTokens).max() ?? 0, compact: true))
Spacer()
Text(L("少"))
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
ForEach([0, Int(Double(goal) * 0.25), Int(Double(goal) * 0.7), goal, goal * 2, goal * 3], id: \.self) { value in
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(contributionColor(tokens: value, goal: goal))
.frame(width: 15, height: 15)
}
Text(L("多"))
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
}
}

private func localizedDays(_ count: Int) -> String {
TokenStepLocalization.language == .en ? "\(count)d" : "\(count) 天"
}

private func monthLabel(_ date: Date) -> String {
let month = Calendar(identifier: .gregorian).component(.month, from: date)
if TokenStepLocalization.language == .en {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.dateFormat = "MMM"
return formatter.string(from: date)
}
return "\(month)月"
}

private func cellHelp(date: String, usage: DailyUsage?) -> String {
let tokens = usage?.totalTokens ?? 0
var lines = ["\(date) · \(TokenStepFormat.tokens(tokens, compact: true))"]
if let tools = usage?.tools, !tools.isEmpty, tokens > 0 {
let parts = orderedToolEntries(tools).prefix(4).map { entry in
let percent = Int((Double(entry.tokens) / Double(tokens) * 100).rounded())
return "\(entry.name) \(percent)%"
}
lines.append(parts.joined(separator: " · "))
}
return lines.joined(separator: "\n")
}
}

func contributionColor(tokens: Int, goal: Int) -> Color {
guard tokens > 0 else { return Color.tokenTrack }
if tokens >= max(goal, 1) {
Expand Down
Loading
Loading