diff --git a/README.md b/README.md
index 5d53f72..24423a0 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.6.dmg)
+[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.7.dmg)
也可以从 Release 页面查看所有版本:
@@ -32,6 +32,23 @@ TokenStep 已使用 Developer ID 签名并通过 Apple 公证。首次打开时
Windows版本由十七做了移植,欢迎大家前往使用:https://github.com/canyexuanfan/TokenStep-Windows/releases
+## 0.2.7 重大更新:引力边界黑洞主题包
+
+0.2.7 新增第三套完整皮肤包「引力边界」。它不是给首页换一张壁纸,而是把近距事件视界、象牙白吸积盘和低速引力光流接进 TokenStep 的整套界面。
+
+
+
+
+
+- **第三套主题皮肤包**:在 `设置 → 通用 → 主题皮肤包` 中,可在经典、奥德赛和引力边界之间随时切换。
+- **黑洞是第一视觉层**:巨大事件视界占据左上,象牙白吸积盘横贯界面,并保留上方透镜弧和更暗的下方反转弧。
+- **全界面统一换肤**:菜单栏浮层、今日、历史、隐私、设置、更新窗口、Token Island 和两类分享卡全部覆盖。
+- **低成本动态光流**:吸积盘有缓慢相位漂移和轻微呼吸;窗口失焦、截图和 macOS“减少动态效果”状态会自动暂停,最高 24 fps。
+- **原创应用内标记**:主题启用时,TokenStep 的应用内 Logo 会切换为事件视界与吸积盘标记。
+- **更新检查更稳**:GitHub API 被限流时自动使用非 API 的最新 Release 通道;手动检查增加 10 秒防连点,并在双通道都失败时显示可理解的限流恢复时间。
+
+主题使用原创生成背景和通用黑洞科学结构,不包含电影 Logo、演员、剧照、飞船或第三方水印。完整说明见 [0.2.7 发布说明](docs/RELEASE_NOTES_0.2.7.md) 与 [引力边界主题实现说明](docs/INTERSTELLAR_THEME_PACK_0.2.7.md)。
+
## 0.2.6 重大更新:检查更新后立即进入安装闭环
0.2.6 修复了从浮层主动检查更新时“查到了,但没有继续弹出更新窗口”的断点,并把下载、验证、替换与自动重启做成可追踪的完整链路。
@@ -85,7 +102,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.6.dmg)。
+打开 `设置 → 通用 → 主题皮肤包` 即可切换。完整说明见 [0.2.4 发布说明](docs/RELEASE_NOTES_0.2.4.md),或直接[下载已签名并通过 Apple 公证的最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.7.dmg)。
## TokenStep 适合谁?
@@ -112,7 +129,7 @@ TokenStep 适合这些人:
- 每日目标可设置,默认每天一个亿。
- 打开面板时按设置的新鲜度刷新;后台在接电时最低 15 分钟、电池或低电量模式下最低 30 分钟刷新,并跳过未变化的数据。
- 开机启动,可在设置里关闭。
-- 主题皮肤包:可随时切回五种经典配色,也可使用奥德赛的导演剪辑、爱琴海冷雾、特洛伊火海与灰烬神像四个视觉篇章。
+- 主题皮肤包:可随时切换经典、奥德赛和引力边界;引力边界覆盖全界面并带低成本黑洞光流动效。
- 一键截图分享当前页面。
- 一键生成「昨日 AI 节奏」分享卡,展示 24 小时使用波形、峰值时段和节奏标签。
- Codex / Claude Code 剩余额度可在设置中打开,默认关闭。
@@ -144,7 +161,7 @@ TokenStep 默认只做本地统计。
## 安装方式
-1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.6.dmg)。
+1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.7.dmg)。
2. 打开 DMG。
3. 把 `TokenStep.app` 拖到「应用程序」。
4. 启动 TokenStep。
@@ -204,7 +221,7 @@ TokenStepSwift/dist/TokenStep.app
Developer ID 签名:
```bash
-TOKENSTEP_VERSION=0.2.6 \
+TOKENSTEP_VERSION=0.2.7 \
CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
./script/package_release.sh
```
@@ -212,7 +229,7 @@ CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
签名 + Apple 公证:
```bash
-TOKENSTEP_VERSION=0.2.6 \
+TOKENSTEP_VERSION=0.2.7 \
CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
TOKENSTEP_NOTARY_PROFILE="tokenstep-notary" \
./script/package_release.sh --notarize
@@ -227,7 +244,7 @@ release/TokenStep-.dmg
维护者说明见 [docs/RELEASE.md](docs/RELEASE.md)。
-0.2.6 发布说明见 [docs/RELEASE_NOTES_0.2.6.md](docs/RELEASE_NOTES_0.2.6.md);0.2.5 动效、模型用量与更新提醒说明见 [docs/RELEASE_NOTES_0.2.5.md](docs/RELEASE_NOTES_0.2.5.md);0.2.4 主题包实现与验收说明见 [docs/ODYSSEY_THEME_PACK_0.2.4.md](docs/ODYSSEY_THEME_PACK_0.2.4.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/Models/UsageModels.swift b/TokenStepSwift/Sources/TokenStepSwift/Models/UsageModels.swift
index accf864..d76fe9b 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Models/UsageModels.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Models/UsageModels.swift
@@ -858,13 +858,18 @@ struct TokenStepSettings: Codable {
}
var themePack: TokenStepThemePack {
- theme == .voyage ? .odyssey : .classic
+ switch theme {
+ case .voyage: return .odyssey
+ case .eventHorizon: return .interstellar
+ default: return .classic
+ }
}
var activeThemeTitle: String {
switch themePack {
case .classic: return classicTheme.title
case .odyssey: return odysseyChapter.title
+ case .interstellar: return L("引力边界")
}
}
@@ -937,7 +942,7 @@ struct TokenStepSettings: Codable {
self.refreshIntervalSeconds = refreshIntervalSeconds
self.historyDays = historyDays
self.theme = theme
- let fallbackClassicTheme = theme == .voyage ? TokenStepTheme.green : theme
+ let fallbackClassicTheme = TokenStepTheme.classicCases.contains(theme) ? theme : .green
let requestedClassicTheme = classicTheme ?? fallbackClassicTheme
self.classicTheme = TokenStepTheme.classicCases.contains(requestedClassicTheme)
? requestedClassicTheme
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Services/UpdateService.swift b/TokenStepSwift/Sources/TokenStepSwift/Services/UpdateService.swift
index a4521cc..341436d 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Services/UpdateService.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Services/UpdateService.swift
@@ -14,7 +14,8 @@ struct AvailableUpdate: Identifiable, Equatable {
var assetSize: Int
var formattedSize: String {
- ByteCountFormatter.string(fromByteCount: Int64(assetSize), countStyle: .file)
+ guard assetSize > 0 else { return L("发布包") }
+ return ByteCountFormatter.string(fromByteCount: Int64(assetSize), countStyle: .file)
}
var noteLines: [String] {
@@ -61,6 +62,7 @@ enum UpdateCheckTrigger: String, Equatable {
enum UpdateCheckPolicy {
static let automaticInterval: TimeInterval = 6 * 60 * 60
static let timerTolerance: TimeInterval = 15 * 60
+ static let manualCooldown: TimeInterval = 10
static func shouldStart(
trigger: UpdateCheckTrigger,
@@ -107,6 +109,7 @@ enum UpdatePresentationPolicy {
struct UpdateCheckGate {
private(set) var isChecking = false
private(set) var lastAttemptAt: Date?
+ private(set) var lastManualAttemptAt: Date?
mutating func begin(
trigger: UpdateCheckTrigger,
@@ -115,6 +118,11 @@ struct UpdateCheckGate {
startupCheckPending: Bool = false
) -> Bool {
guard !isChecking else { return false }
+ if trigger.isManual,
+ let lastManualAttemptAt,
+ now.timeIntervalSince(lastManualAttemptAt) < UpdateCheckPolicy.manualCooldown {
+ return false
+ }
guard UpdateCheckPolicy.shouldStart(
trigger: trigger,
autoUpdateEnabled: autoUpdateEnabled,
@@ -126,6 +134,9 @@ struct UpdateCheckGate {
}
isChecking = true
lastAttemptAt = now
+ if trigger.isManual {
+ lastManualAttemptAt = now
+ }
return true
}
@@ -136,6 +147,8 @@ struct UpdateCheckGate {
enum UpdateService {
private static let latestReleaseURL = URL(string: "https://api.github.com/repos/Backtthefuture/TokenStep/releases/latest")!
+ private static let latestReleaseFallbackURL = URL(string: "https://github.com/Backtthefuture/TokenStep/releases/latest")!
+ private static let releaseDownloadBaseURL = URL(string: "https://github.com/Backtthefuture/TokenStep/releases/download/")!
static var currentVersion: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
@@ -144,7 +157,8 @@ enum UpdateService {
static func checkForUpdates(
currentVersion: String = Self.currentVersion,
session: URLSession = .shared,
- releaseURL: URL? = nil
+ releaseURL: URL? = nil,
+ fallbackURL: URL? = nil
) async throws -> UpdateCheckResult {
var request = URLRequest(url: releaseURL ?? latestReleaseURL)
request.cachePolicy = .reloadIgnoringLocalCacheData
@@ -152,18 +166,123 @@ enum UpdateService {
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
request.setValue("TokenStep/\(currentVersion)", forHTTPHeaderField: "User-Agent")
- let (data, response) = try await session.data(for: request)
+ do {
+ let (data, response) = try await session.data(for: request)
+ guard let http = response as? HTTPURLResponse else {
+ throw UpdateError.checkFailed
+ }
+
+ if (200..<300).contains(http.statusCode) {
+ return try evaluateReleaseResponse(
+ data: data,
+ statusCode: http.statusCode,
+ currentVersion: currentVersion
+ )
+ }
+
+ let primaryError = responseError(from: http)
+ do {
+ return try await checkFallbackLatestRelease(
+ currentVersion: currentVersion,
+ session: session,
+ fallbackURL: fallbackURL
+ )
+ } catch {
+ throw primaryError
+ }
+ } catch let error as UpdateError {
+ throw error
+ } catch {
+ do {
+ return try await checkFallbackLatestRelease(
+ currentVersion: currentVersion,
+ session: session,
+ fallbackURL: fallbackURL
+ )
+ } catch {
+ throw UpdateError.checkFailed
+ }
+ }
+ }
+
+ private static func checkFallbackLatestRelease(
+ currentVersion: String,
+ session: URLSession,
+ fallbackURL: URL?
+ ) async throws -> UpdateCheckResult {
+ var request = URLRequest(url: fallbackURL ?? latestReleaseFallbackURL)
+ request.cachePolicy = .reloadIgnoringLocalCacheData
+ request.timeoutInterval = 15
+ request.setValue("text/html,application/xhtml+xml", forHTTPHeaderField: "Accept")
+ request.setValue("TokenStep/\(currentVersion)", forHTTPHeaderField: "User-Agent")
+
+ let (_, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw UpdateError.checkFailed
}
-
- return try evaluateReleaseResponse(
- data: data,
+ return try evaluateFallbackReleaseResponse(
+ responseURL: http.url,
statusCode: http.statusCode,
currentVersion: currentVersion
)
}
+ static func evaluateFallbackReleaseResponse(
+ responseURL: URL?,
+ statusCode: Int,
+ currentVersion: String
+ ) throws -> UpdateCheckResult {
+ guard (200..<300).contains(statusCode),
+ let responseURL,
+ let tagName = releaseTag(from: responseURL),
+ let releaseVersion = Version(tagName.strippingVersionPrefix),
+ let installedVersion = Version(currentVersion)
+ else {
+ throw UpdateError.checkFailed
+ }
+
+ guard releaseVersion > installedVersion else { return .upToDate }
+ let version = tagName.strippingVersionPrefix
+ let assetName = "TokenStep-\(version).dmg"
+ let assetURL = releaseDownloadBaseURL
+ .appendingPathComponent(tagName, isDirectory: true)
+ .appendingPathComponent(assetName)
+
+ return .available(
+ AvailableUpdate(
+ version: version,
+ tagName: tagName,
+ title: "TokenStep \(version)",
+ notes: L("发现新的稳定版本,可直接下载并安装已签名公证的 DMG。"),
+ pageURL: responseURL,
+ assetURL: assetURL,
+ assetName: assetName,
+ assetSize: 0
+ )
+ )
+ }
+
+ private static func releaseTag(from url: URL) -> String? {
+ let marker = "/releases/tag/"
+ guard let range = url.path.range(of: marker) else { return nil }
+ let encoded = String(url.path[range.upperBound...])
+ .trimmingCharacters(in: CharacterSet(charactersIn: "/"))
+ guard !encoded.isEmpty, !encoded.contains("/") else { return nil }
+ return encoded.removingPercentEncoding ?? encoded
+ }
+
+ private static func responseError(from response: HTTPURLResponse) -> UpdateError {
+ let remaining = response.value(forHTTPHeaderField: "X-RateLimit-Remaining")
+ let isRateLimited = response.statusCode == 429
+ || (response.statusCode == 403 && remaining == "0")
+ guard isRateLimited else { return .checkFailed }
+
+ let resetAt = response.value(forHTTPHeaderField: "X-RateLimit-Reset")
+ .flatMap(TimeInterval.init)
+ .map(Date.init(timeIntervalSince1970:))
+ return .rateLimited(resetAt: resetAt)
+ }
+
static func evaluateReleaseResponse(
data: Data,
statusCode: Int,
@@ -527,6 +646,7 @@ enum UpdateService {
enum UpdateError: LocalizedError, Equatable {
case checkFailed
+ case rateLimited(resetAt: Date?)
case missingDMG
case downloadFailed
case verificationFailed
@@ -536,6 +656,13 @@ enum UpdateError: LocalizedError, Equatable {
switch self {
case .checkFailed:
return L("检查更新失败,请稍后再试。")
+ case let .rateLimited(resetAt):
+ guard let resetAt else {
+ return L("GitHub 检查频率暂时受限,请稍后再试。")
+ }
+ let formatter = DateFormatter()
+ formatter.dateFormat = "HH:mm"
+ return LFormat("GitHub 检查频率暂时受限,预计 %@ 后恢复。", formatter.string(from: resetAt))
case .missingDMG:
return L("新版本没有可下载的 DMG。")
case .downloadFailed:
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Stores/AppState.swift b/TokenStepSwift/Sources/TokenStepSwift/Stores/AppState.swift
index f5dfce9..fae8eb2 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Stores/AppState.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Stores/AppState.swift
@@ -423,11 +423,11 @@ final class AppState: ObservableObject {
}
func setTheme(_ theme: TokenStepTheme) {
- if theme == .voyage {
- settings.theme = .voyage
- } else {
+ if TokenStepTheme.classicCases.contains(theme) {
settings.classicTheme = theme
settings.theme = theme
+ } else {
+ settings.theme = theme
}
TokenStepThemeRuntime.apply(
settings.theme,
@@ -437,7 +437,14 @@ final class AppState: ObservableObject {
}
func setThemePack(_ pack: TokenStepThemePack) {
- settings.theme = pack == .odyssey ? .voyage : settings.classicTheme
+ switch pack {
+ case .classic:
+ settings.theme = settings.classicTheme
+ case .odyssey:
+ settings.theme = .voyage
+ case .interstellar:
+ settings.theme = .eventHorizon
+ }
TokenStepThemeRuntime.apply(
settings.theme,
odysseyChapter: settings.odysseyChapter
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Support/Formatters.swift b/TokenStepSwift/Sources/TokenStepSwift/Support/Formatters.swift
index cf7dc12..2997a2d 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Support/Formatters.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Support/Formatters.swift
@@ -109,44 +109,44 @@ extension Color {
static var tokenGreen: Color { TokenStepThemeRuntime.palette.accent.color }
static var tokenGreenDark: Color { TokenStepThemeRuntime.palette.accentDark.color }
static var tokenToggleTint: Color {
- TokenStepThemeRuntime.isVoyage ? tokenGreenDark : tokenGreen
+ TokenStepThemeRuntime.isCinematic ? tokenGreenDark : tokenGreen
}
static var tokenMint: Color { TokenStepThemeRuntime.palette.accentSoft.color }
static var tokenTrack: Color { TokenStepThemeRuntime.palette.track.color }
static var tokenLowActivity: Color { TokenStepThemeRuntime.palette.lowActivity.color }
static var tokenDivider: Color { TokenStepThemeRuntime.theme.dividerColor }
static var tokenHairline: Color {
- TokenStepThemeRuntime.isVoyage
+ TokenStepThemeRuntime.isCinematic
? TokenStepThemeRuntime.palette.accentDark.color.opacity(0.22)
: Color.black.opacity(0.055)
}
static var tokenHairlineStrong: Color {
- TokenStepThemeRuntime.isVoyage
+ TokenStepThemeRuntime.isCinematic
? TokenStepThemeRuntime.palette.accent.color.opacity(0.48)
: Color.black.opacity(0.09)
}
static var tokenInnerHighlight: Color {
- TokenStepThemeRuntime.isVoyage
+ TokenStepThemeRuntime.isCinematic
? Color(red: 240 / 255, green: 209 / 255, blue: 156 / 255).opacity(0.08)
: Color.white.opacity(0.42)
}
static var tokenShadow: Color {
- TokenStepThemeRuntime.isVoyage
+ TokenStepThemeRuntime.isCinematic
? Color.black.opacity(0.34)
: Color.black.opacity(0.055)
}
static var tokenActionText: Color {
- TokenStepThemeRuntime.isVoyage
+ TokenStepThemeRuntime.isCinematic
? Color(red: 22 / 255, green: 18 / 255, blue: 14 / 255)
: Color.white
}
static var tokenMutedFill: Color {
- TokenStepThemeRuntime.isVoyage
+ TokenStepThemeRuntime.isCinematic
? TokenStepThemeRuntime.palette.track.color.opacity(0.52)
: TokenStepThemeRuntime.palette.track.color.opacity(0.42)
}
static var tokenWarning: Color {
- TokenStepThemeRuntime.isVoyage
+ TokenStepThemeRuntime.isCinematic
? Color(red: 218 / 255, green: 139 / 255, blue: 67 / 255)
: Color.orange
}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift b/TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift
index b7faa5a..242f4d0 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift
@@ -372,8 +372,10 @@ enum TokenStepLocalization {
"远征": "Voyage",
"经典": "Classic",
"奥德赛": "Odyssey",
+ "星际": "Interstellar",
"原版 TokenStep": "Original TokenStep",
"冷雾 · 骨金 · 火星": "Mist · Bone Gold · Embers",
+ "事件视界 · 象牙光": "Event Horizon · Ivory Light",
"导演剪辑": "Director's Cut",
"爱琴海冷雾": "Aegean Mist",
"特洛伊火海": "Trojan Inferno",
@@ -383,9 +385,13 @@ enum TokenStepLocalization {
"木马 · 烈火 · 余烬": "Horse · Fire · Embers",
"神像 · 石纹 · 灰烬": "Relief · Marble · Ash",
"主题皮肤包": "Theme Skin Pack",
- "经典配色与奥德赛视觉篇章可随时切换": "Switch between Classic colors and Odyssey chapters anytime",
+ "经典、奥德赛与引力边界可随时切换": "Switch between Classic, Odyssey, and Event Horizon anytime",
"奥德赛视觉篇章": "Odyssey Visual Chapter",
"导演剪辑会为不同界面自动分配冷雾、火海与灰烬。": "Director's Cut assigns mist, fire, and ash to the surfaces where they work best.",
+ "引力边界": "Event Horizon",
+ "引力边界 · 近距事件视界": "Event Horizon · Close Approach",
+ "巨大黑洞、象牙白吸积盘与克制电影色;低亮与减少动态时自动静止。": "A monumental black hole, ivory accretion disk, and restrained cinema color; motion pauses in low-light and Reduce Motion states.",
+ "原创动态主题": "Original Motion Theme",
"经典主题色": "Classic Theme Color",
"原版 TokenStep 的五种明亮配色": "Five bright palettes from the original TokenStep",
"默认": "Default",
@@ -669,14 +675,20 @@ enum TokenStepLocalization {
"省电策略": "Power saving",
"电池或低电量时后台最短 30 分钟": "On battery or Low Power Mode, background refresh is at least 30 minutes",
"更新与启动": "Updates and startup",
+ "发布包": "Release package",
+ "GitHub 检查频率暂时受限,请稍后再试。": "GitHub update checks are temporarily rate-limited. Please try again later.",
+ "GitHub 检查频率暂时受限,预计 %@ 后恢复。": "GitHub update checks are rate-limited and should recover after %@.",
+ "发现新的稳定版本,可直接下载并安装已签名公证的 DMG。": "A new stable release is available. Download and install the signed, notarized DMG.",
"自动检查更新": "Check for updates automatically",
"榜": "Rank",
],
.zhHant: [
"经典": "經典",
"奥德赛": "奧德賽",
+ "星际": "星際",
"原版 TokenStep": "原版 TokenStep",
"冷雾 · 骨金 · 火星": "冷霧 · 骨金 · 火星",
+ "事件视界 · 象牙光": "事件視界 · 象牙光",
"导演剪辑": "導演剪輯",
"爱琴海冷雾": "愛琴海冷霧",
"特洛伊火海": "特洛伊火海",
@@ -686,9 +698,17 @@ enum TokenStepLocalization {
"木马 · 烈火 · 余烬": "木馬 · 烈火 · 餘燼",
"神像 · 石纹 · 灰烬": "神像 · 石紋 · 灰燼",
"主题皮肤包": "主題皮膚包",
- "经典配色与奥德赛视觉篇章可随时切换": "經典配色與奧德賽視覺篇章可隨時切換",
+ "发布包": "發佈包",
+ "GitHub 检查频率暂时受限,请稍后再试。": "GitHub 檢查頻率暫時受限,請稍後再試。",
+ "GitHub 检查频率暂时受限,预计 %@ 后恢复。": "GitHub 檢查頻率暫時受限,預計 %@ 後恢復。",
+ "发现新的稳定版本,可直接下载并安装已签名公证的 DMG。": "發現新的穩定版本,可直接下載並安裝已簽名公證的 DMG。",
+ "经典、奥德赛与引力边界可随时切换": "經典、奧德賽與引力邊界可隨時切換",
"奥德赛视觉篇章": "奧德賽視覺篇章",
"导演剪辑会为不同界面自动分配冷雾、火海与灰烬。": "導演剪輯會為不同介面自動分配冷霧、火海與灰燼。",
+ "引力边界": "引力邊界",
+ "引力边界 · 近距事件视界": "引力邊界 · 近距事件視界",
+ "巨大黑洞、象牙白吸积盘与克制电影色;低亮与减少动态时自动静止。": "巨大黑洞、象牙白吸積盤與克制電影色;低亮與減少動態時自動靜止。",
+ "原创动态主题": "原創動態主題",
"经典主题色": "經典主題色",
"原版 TokenStep 的五种明亮配色": "原版 TokenStep 的五種明亮配色",
"切换到%@主题包": "切換到%@主題包",
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Support/Theme.swift b/TokenStepSwift/Sources/TokenStepSwift/Support/Theme.swift
index 3b79b52..2de2551 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Support/Theme.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Support/Theme.swift
@@ -54,6 +54,7 @@ struct TokenStepThemePalette {
enum TokenStepThemePack: String, CaseIterable, Identifiable, Codable {
case classic
case odyssey
+ case interstellar
var id: String { rawValue }
@@ -61,6 +62,7 @@ enum TokenStepThemePack: String, CaseIterable, Identifiable, Codable {
switch self {
case .classic: return L("经典")
case .odyssey: return L("奥德赛")
+ case .interstellar: return L("引力边界")
}
}
@@ -68,6 +70,7 @@ enum TokenStepThemePack: String, CaseIterable, Identifiable, Codable {
switch self {
case .classic: return L("原版 TokenStep")
case .odyssey: return L("冷雾 · 骨金 · 火星")
+ case .interstellar: return L("事件视界 · 象牙光")
}
}
}
@@ -201,6 +204,7 @@ enum TokenStepTheme: String, CaseIterable, Identifiable, Codable {
case amber
case graphite
case voyage
+ case eventHorizon = "event_horizon"
static let classicCases: [TokenStepTheme] = [.green, .ocean, .violet, .amber, .graphite]
@@ -214,6 +218,7 @@ enum TokenStepTheme: String, CaseIterable, Identifiable, Codable {
case .amber: return L("琥珀")
case .graphite: return L("石墨")
case .voyage: return L("奥德赛")
+ case .eventHorizon: return L("引力边界")
}
}
@@ -225,23 +230,34 @@ enum TokenStepTheme: String, CaseIterable, Identifiable, Codable {
case .amber: return L("温暖")
case .graphite: return L("专注")
case .voyage: return L("主题皮肤包")
+ case .eventHorizon: return L("主题皮肤包")
}
}
var colorScheme: ColorScheme {
- self == .voyage ? .dark : .light
+ self == .voyage || self == .eventHorizon ? .dark : .light
}
var inkColor: Color {
- self == .voyage
- ? Color(red: 231 / 255, green: 221 / 255, blue: 200 / 255)
- : Color(red: 31 / 255, green: 41 / 255, blue: 55 / 255)
+ switch self {
+ case .voyage:
+ return Color(red: 231 / 255, green: 221 / 255, blue: 200 / 255)
+ case .eventHorizon:
+ return Color(red: 244 / 255, green: 239 / 255, blue: 226 / 255)
+ default:
+ return Color(red: 31 / 255, green: 41 / 255, blue: 55 / 255)
+ }
}
var dividerColor: Color {
- self == .voyage
- ? Color(red: 116 / 255, green: 82 / 255, blue: 49 / 255).opacity(0.58)
- : Color.black.opacity(0.06)
+ switch self {
+ case .voyage:
+ return Color(red: 116 / 255, green: 82 / 255, blue: 49 / 255).opacity(0.58)
+ case .eventHorizon:
+ return Color(red: 194 / 255, green: 177 / 255, blue: 143 / 255).opacity(0.34)
+ default:
+ return Color.black.opacity(0.06)
+ }
}
var palette: TokenStepThemePalette {
@@ -338,6 +354,24 @@ enum TokenStepTheme: String, CaseIterable, Identifiable, Codable {
)
case .voyage:
return TokenStepOdysseyChapter.directorsCut.palette
+ case .eventHorizon:
+ return TokenStepThemePalette(
+ canvas: .init(red: 5 / 255, green: 8 / 255, blue: 13 / 255),
+ surface: .init(red: 18 / 255, green: 21 / 255, blue: 25 / 255),
+ accent: .init(red: 224 / 255, green: 207 / 255, blue: 169 / 255),
+ accentDark: .init(red: 251 / 255, green: 241 / 255, blue: 213 / 255),
+ accentSoft: .init(red: 87 / 255, green: 80 / 255, blue: 67 / 255),
+ track: .init(red: 47 / 255, green: 49 / 255, blue: 51 / 255),
+ lowActivity: .init(red: 58 / 255, green: 58 / 255, blue: 57 / 255),
+ activity1: .init(red: 101 / 255, green: 96 / 255, blue: 86 / 255),
+ activity2: .init(red: 148 / 255, green: 137 / 255, blue: 113 / 255),
+ activity3: .init(red: 199 / 255, green: 182 / 255, blue: 146 / 255),
+ activity4: .init(red: 244 / 255, green: 231 / 255, blue: 198 / 255),
+ ring1: .init(red: 224 / 255, green: 207 / 255, blue: 169 / 255),
+ ring2: .init(red: 236 / 255, green: 220 / 255, blue: 183 / 255),
+ ring3: .init(red: 247 / 255, green: 234 / 255, blue: 204 / 255),
+ ring4: .init(red: 255 / 255, green: 247 / 255, blue: 224 / 255)
+ )
}
}
}
@@ -351,9 +385,15 @@ enum TokenStepThemeRuntime {
isOdyssey ? activeOdysseyChapter.palette : activeTheme.palette
}
static var isOdyssey: Bool { activeTheme == .voyage }
+ static var isInterstellar: Bool { activeTheme == .eventHorizon }
+ static var isCinematic: Bool { isOdyssey || isInterstellar }
static var isVoyage: Bool { isOdyssey }
static var odysseyChapter: TokenStepOdysseyChapter { activeOdysseyChapter }
- static var themePack: TokenStepThemePack { isOdyssey ? .odyssey : .classic }
+ static var themePack: TokenStepThemePack {
+ if isOdyssey { return .odyssey }
+ if isInterstellar { return .interstellar }
+ return .classic
+ }
static func apply(
_ theme: TokenStepTheme,
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/AgentWorkViews.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/AgentWorkViews.swift
index 586f3a2..c0f2163 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/AgentWorkViews.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/AgentWorkViews.swift
@@ -88,7 +88,7 @@ struct TodayAgentWorkCard: View {
value: "\(selectedActiveHours)/\(period.maximumHours)",
detail: AgentWorkCopy.recordedHourDetail,
symbol: "clock.fill",
- color: TokenStepThemeRuntime.isVoyage
+ color: TokenStepThemeRuntime.isCinematic
? TokenStepThemeRuntime.palette.activity2.color
: Color(red: 0.20, green: 0.52, blue: 0.92)
)
@@ -97,7 +97,7 @@ struct TodayAgentWorkCard: View {
value: TokenStepFormat.tokens(selectedSevenDayAverage, compact: true),
detail: AgentWorkCopy.calendarDayAverage,
symbol: "calendar.badge.clock",
- color: TokenStepThemeRuntime.isVoyage
+ color: TokenStepThemeRuntime.isCinematic
? TokenStepThemeRuntime.palette.activity3.color
: Color(red: 0.50, green: 0.28, blue: 0.92)
)
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Components.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Components.swift
index adb8d71..043493b 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Components.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Components.swift
@@ -43,7 +43,9 @@ struct TokenStepBackdrop: View {
var body: some View {
ZStack {
Color.tokenCanvas
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isInterstellar {
+ InterstellarBackdrop(role: role)
+ } else if TokenStepThemeRuntime.isVoyage {
VoyageBackdropAtmosphere(role: role)
} else {
LinearGradient(
@@ -65,7 +67,9 @@ struct TokenStepMark: View {
var size: CGFloat = 48
var body: some View {
- if let icon = TokenStepAppIconImage.image {
+ if TokenStepThemeRuntime.isInterstellar {
+ InterstellarTokenStepMark(size: size)
+ } else if let icon = TokenStepAppIconImage.image {
Image(nsImage: icon)
.resizable()
.interpolation(.high)
@@ -202,7 +206,7 @@ struct UsageRecalibrationNotice: View {
}
.padding(.horizontal, 14)
.padding(.vertical, 11)
- .background(Color.tokenSuccess.opacity(TokenStepThemeRuntime.isVoyage ? 0.12 : 0.10), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
+ .background(Color.tokenSuccess.opacity(TokenStepThemeRuntime.isCinematic ? 0.12 : 0.10), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.tokenSuccess.opacity(0.24))
@@ -264,7 +268,7 @@ struct TokenCard: View {
.background {
ZStack {
shape.fill(Color.tokenSurface)
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
LinearGradient(
colors: [
Color.tokenGreen.opacity(0.055),
@@ -283,6 +287,8 @@ struct TokenCard: View {
shape.stroke(Color.tokenHairline)
if TokenStepThemeRuntime.isVoyage {
VoyageCardOrnament(cornerRadius: 24)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarCardOrnament(cornerRadius: 24)
}
}
}
@@ -724,7 +730,7 @@ func contributionColor(tokens: Int, goal: Int) -> Color {
}
func tokenToolColor(_ tool: String) -> Color {
- if TokenStepThemeRuntime.theme == .voyage {
+ if TokenStepThemeRuntime.isCinematic {
let value = tool.lowercased()
let palette = TokenStepThemeRuntime.palette
if value.contains("codex") { return palette.activity4.color }
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/CursorCodeSignalCard.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/CursorCodeSignalCard.swift
index 09a42eb..00b4439 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/CursorCodeSignalCard.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/CursorCodeSignalCard.swift
@@ -49,12 +49,12 @@ struct CursorCodeSignalCard: View {
}
.overlay(
RoundedRectangle(cornerRadius: 24, style: .continuous)
- .stroke(TokenStepThemeRuntime.isVoyage ? Color.tokenHairlineStrong : Color(red: 0.42, green: 0.36, blue: 0.82).opacity(0.18))
+ .stroke(TokenStepThemeRuntime.isCinematic ? Color.tokenHairlineStrong : Color(red: 0.42, green: 0.36, blue: 0.82).opacity(0.18))
)
}
private func modelDot(_ index: Int) -> Color {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
let colors = [
TokenStepThemeRuntime.palette.activity4.color,
TokenStepThemeRuntime.palette.activity3.color,
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/HistoryView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/HistoryView.swift
index e8928b3..361111f 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/HistoryView.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/HistoryView.swift
@@ -38,6 +38,11 @@ struct HistoryView: View {
.frame(width: 260, height: 116)
.padding(18)
.opacity(0.46)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarEventHorizonEmblem()
+ .frame(width: 260, height: 108)
+ .padding(18)
+ .opacity(0.38)
}
}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/InterstellarSkin.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/InterstellarSkin.swift
new file mode 100644
index 0000000..70ccbc4
--- /dev/null
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/InterstellarSkin.swift
@@ -0,0 +1,365 @@
+import AppKit
+import SwiftUI
+
+enum InterstellarMotionPolicy {
+ static let preferredFramesPerSecond = 24
+
+ static func shouldAnimate(
+ reduceMotion: Bool,
+ isScreenshotRendering: Bool,
+ isControlActive: Bool
+ ) -> Bool {
+ !reduceMotion && !isScreenshotRendering && isControlActive
+ }
+}
+
+enum InterstellarArtwork {
+ private static let hero = load(
+ resource: "InterstellarEventHorizonHero",
+ environmentKey: "TOKENSTEP_INTERSTELLAR_HERO_ART_PATH"
+ )
+ private static let quiet = load(
+ resource: "InterstellarEventHorizonQuiet",
+ environmentKey: "TOKENSTEP_INTERSTELLAR_QUIET_ART_PATH"
+ )
+
+ static func image(quiet prefersQuiet: Bool) -> NSImage? {
+ prefersQuiet ? (quiet ?? hero) : (hero ?? quiet)
+ }
+
+ private static func load(resource: String, environmentKey: String) -> NSImage? {
+ if let path = ProcessInfo.processInfo.environment[environmentKey],
+ !path.isEmpty,
+ let image = NSImage(contentsOfFile: path) {
+ return image
+ }
+ if let url = Bundle.main.url(forResource: resource, withExtension: "png"),
+ let image = NSImage(contentsOf: url) {
+ return image
+ }
+ return NSImage(named: resource)
+ }
+}
+
+struct InterstellarBackdrop: View {
+ var role: OdysseySurfaceRole = .generic
+ var isScreenshotRendering = false
+
+ @Environment(\.accessibilityReduceMotion) private var reduceMotion
+ @Environment(\.controlActiveState) private var controlActiveState
+
+ private var usesQuietArtwork: Bool {
+ reduceMotion || [.history, .privacy, .settings, .update].contains(role)
+ }
+
+ private var motionEnabled: Bool {
+ InterstellarMotionPolicy.shouldAnimate(
+ reduceMotion: reduceMotion,
+ isScreenshotRendering: isScreenshotRendering,
+ isControlActive: controlActiveState != .inactive
+ )
+ }
+
+ var body: some View {
+ GeometryReader { proxy in
+ ZStack {
+ Color(red: 4 / 255, green: 7 / 255, blue: 12 / 255)
+
+ if let artwork = InterstellarArtwork.image(quiet: usesQuietArtwork) {
+ Image(nsImage: artwork)
+ .resizable()
+ .interpolation(.high)
+ .scaledToFill()
+ .frame(width: proxy.size.width, height: proxy.size.height)
+ .clipped()
+ } else {
+ InterstellarFallbackPlate()
+ }
+
+ InterstellarAccretionMotionLayer(isActive: motionEnabled)
+
+ Color.black.opacity(roleVeilOpacity)
+
+ LinearGradient(
+ colors: [
+ Color.black.opacity(0.10),
+ Color.clear,
+ Color(red: 4 / 255, green: 7 / 255, blue: 12 / 255).opacity(0.62)
+ ],
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+
+ LinearGradient(
+ colors: [Color.clear, Color.black.opacity(bottomVeilOpacity)],
+ startPoint: .center,
+ endPoint: .bottom
+ )
+ }
+ .frame(width: proxy.size.width, height: proxy.size.height)
+ }
+ .ignoresSafeArea()
+ .allowsHitTesting(false)
+ .accessibilityHidden(true)
+ }
+
+ private var roleVeilOpacity: Double {
+ switch role {
+ case .popover: return 0.16
+ case .dashboard: return 0.28
+ case .share: return 0.12
+ case .history: return 0.43
+ case .privacy: return 0.52
+ case .settings: return 0.50
+ case .update: return 0.42
+ case .island: return 0.30
+ case .generic: return 0.36
+ }
+ }
+
+ private var bottomVeilOpacity: Double {
+ switch role {
+ case .popover, .share: return 0.22
+ default: return 0.44
+ }
+ }
+}
+
+private struct InterstellarAccretionMotionLayer: View {
+ var isActive: Bool
+
+ var body: some View {
+ TimelineView(
+ .animation(
+ minimumInterval: 1 / Double(InterstellarMotionPolicy.preferredFramesPerSecond),
+ paused: !isActive
+ )
+ ) { timeline in
+ let time = timeline.date.timeIntervalSinceReferenceDate
+ GeometryReader { proxy in
+ let phase = CGFloat(time.truncatingRemainder(dividingBy: 36) / 36)
+ let breath = 0.5 + 0.5 * sin(time * 0.22)
+ ZStack {
+ RadialGradient(
+ colors: [
+ Color(red: 1.0, green: 0.96, blue: 0.84).opacity(0.075 + breath * 0.035),
+ Color.clear
+ ],
+ center: UnitPoint(x: 0.42, y: 0.38),
+ startRadius: 2,
+ endRadius: max(proxy.size.width, proxy.size.height) * 0.28
+ )
+ .blendMode(.screen)
+
+ LinearGradient(
+ stops: [
+ .init(color: .clear, location: 0.00),
+ .init(color: Color.white.opacity(0.015), location: 0.35),
+ .init(color: Color(red: 1.0, green: 0.95, blue: 0.80).opacity(0.13), location: 0.49),
+ .init(color: Color.white.opacity(0.018), location: 0.64),
+ .init(color: .clear, location: 1.00)
+ ],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ .frame(width: proxy.size.width * 0.72, height: max(2, proxy.size.height * 0.012))
+ .blur(radius: 2.4)
+ .offset(
+ x: -proxy.size.width * 0.36 + phase * proxy.size.width * 1.08,
+ y: proxy.size.height * 0.015
+ )
+ .blendMode(.screen)
+
+ LinearGradient(
+ colors: [Color.clear, Color.white.opacity(0.055), Color.clear],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ .frame(width: proxy.size.width * 0.34, height: 1)
+ .offset(
+ x: proxy.size.width * (0.18 - phase * 0.42),
+ y: proxy.size.height * 0.055
+ )
+ .blendMode(.screen)
+ }
+ .frame(width: proxy.size.width, height: proxy.size.height)
+ }
+ }
+ .opacity(isActive ? 1 : 0)
+ }
+}
+
+private struct InterstellarFallbackPlate: View {
+ var body: some View {
+ GeometryReader { proxy in
+ ZStack {
+ LinearGradient(
+ colors: [Color.black, Color(red: 8 / 255, green: 13 / 255, blue: 20 / 255)],
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ Circle()
+ .fill(Color.black)
+ .frame(width: proxy.size.width * 0.72)
+ .offset(x: -proxy.size.width * 0.30, y: -proxy.size.height * 0.18)
+ Capsule()
+ .fill(
+ LinearGradient(
+ colors: [Color.clear, Color(red: 1.0, green: 0.95, blue: 0.82), Color.clear],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ )
+ .frame(width: proxy.size.width * 1.15, height: 4)
+ .shadow(color: Color.white.opacity(0.55), radius: 14)
+ }
+ }
+ }
+}
+
+struct InterstellarPanelBackground: View {
+ var opacity: Double = 0.70
+ var cornerRadius: CGFloat = 18
+
+ var body: some View {
+ let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
+ shape
+ .fill(Color(red: 8 / 255, green: 11 / 255, blue: 16 / 255).opacity(opacity))
+ .overlay(shape.stroke(Color.tokenHairlineStrong.opacity(0.82), lineWidth: 1))
+ .overlay(alignment: .topLeading) {
+ LinearGradient(
+ colors: [Color.tokenGreenDark.opacity(0.70), Color.tokenGreen.opacity(0.08), Color.clear],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ .frame(width: 128, height: 1)
+ .padding(.leading, 18)
+ }
+ .shadow(color: Color.black.opacity(0.30), radius: 18, y: 9)
+ }
+}
+
+struct InterstellarWindowFrame: View {
+ var inset: CGFloat = 8
+
+ var body: some View {
+ GeometryReader { proxy in
+ let shape = RoundedRectangle(cornerRadius: 20, style: .continuous)
+ ZStack {
+ shape
+ .stroke(Color.tokenHairlineStrong.opacity(0.78), lineWidth: 1)
+ .padding(inset)
+ ForEach(0..<4, id: \.self) { index in
+ let x = index < 2 ? inset + 16 : proxy.size.width - inset - 16
+ let y = index.isMultiple(of: 2) ? inset + 1 : proxy.size.height - inset - 1
+ Capsule()
+ .fill(Color.tokenGreenDark.opacity(0.66))
+ .frame(width: min(120, proxy.size.width * 0.16), height: 1)
+ .position(x: x + (index < 2 ? 48 : -48), y: y)
+ }
+ }
+ }
+ .allowsHitTesting(false)
+ .accessibilityHidden(true)
+ }
+}
+
+struct InterstellarCardOrnament: View {
+ var cornerRadius: CGFloat = 24
+
+ var body: some View {
+ RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
+ .stroke(Color.tokenInnerHighlight.opacity(0.72), lineWidth: 1)
+ .padding(1)
+ .overlay(alignment: .topLeading) {
+ Capsule()
+ .fill(Color.tokenGreenDark.opacity(0.64))
+ .frame(width: 82, height: 1)
+ .padding(.leading, 20)
+ }
+ .allowsHitTesting(false)
+ }
+}
+
+struct InterstellarEventHorizonEmblem: View {
+ var body: some View {
+ GeometryReader { proxy in
+ ZStack {
+ Ellipse()
+ .stroke(Color.tokenGreenDark.opacity(0.42), lineWidth: 2)
+ .frame(width: proxy.size.width * 0.92, height: proxy.size.height * 0.24)
+ .rotationEffect(.degrees(-5))
+ Circle()
+ .fill(Color.black.opacity(0.96))
+ .overlay(Circle().stroke(Color.tokenGreenDark.opacity(0.54), lineWidth: 1.5))
+ .frame(width: min(proxy.size.width, proxy.size.height) * 0.54)
+ Capsule()
+ .fill(
+ LinearGradient(
+ colors: [Color.clear, Color.tokenGreenDark.opacity(0.9), Color.clear],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ )
+ .frame(width: proxy.size.width, height: 2)
+ }
+ }
+ .allowsHitTesting(false)
+ .accessibilityHidden(true)
+ }
+}
+
+struct InterstellarTokenStepMark: View {
+ var size: CGFloat
+
+ var body: some View {
+ ZStack {
+ RoundedRectangle(cornerRadius: size * 0.27, style: .continuous)
+ .fill(Color(red: 7 / 255, green: 10 / 255, blue: 15 / 255))
+ .overlay(
+ RoundedRectangle(cornerRadius: size * 0.27, style: .continuous)
+ .stroke(Color.tokenGreen.opacity(0.50), lineWidth: max(0.8, size * 0.02))
+ )
+ Ellipse()
+ .stroke(Color.tokenGreenDark.opacity(0.86), lineWidth: max(1.2, size * 0.055))
+ .frame(width: size * 0.72, height: size * 0.20)
+ .shadow(color: Color.tokenGreenDark.opacity(0.30), radius: size * 0.08)
+ Circle()
+ .fill(Color.black)
+ .overlay(Circle().stroke(Color.tokenGreenDark.opacity(0.72), lineWidth: max(0.7, size * 0.025)))
+ .frame(width: size * 0.34, height: size * 0.34)
+ Capsule()
+ .fill(Color.tokenGreenDark)
+ .frame(width: size * 0.68, height: max(1, size * 0.035))
+ }
+ .frame(width: size, height: size)
+ }
+}
+
+struct InterstellarThemePreview: View {
+ var body: some View {
+ ZStack {
+ RoundedRectangle(cornerRadius: 13, style: .continuous)
+ .fill(
+ LinearGradient(
+ colors: [Color.black, Color(red: 11 / 255, green: 16 / 255, blue: 24 / 255)],
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ )
+ Ellipse()
+ .stroke(Color.tokenGreenDark.opacity(0.88), lineWidth: 3)
+ .frame(width: 50, height: 13)
+ .offset(x: -7)
+ Circle()
+ .fill(Color.black)
+ .frame(width: 20, height: 20)
+ .offset(x: -8)
+ Capsule()
+ .fill(Color.tokenGreenDark)
+ .frame(width: 51, height: 2)
+ .offset(x: 2)
+ }
+ .overlay(RoundedRectangle(cornerRadius: 13, style: .continuous).stroke(Color.tokenGreen.opacity(0.48)))
+ }
+}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/MainWindowView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/MainWindowView.swift
index 9aea5c3..9e17d27 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/MainWindowView.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/MainWindowView.swift
@@ -81,6 +81,8 @@ struct MainWindowView: View {
.overlay {
if TokenStepThemeRuntime.isVoyage {
VoyageWindowFrame(inset: 8)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarWindowFrame(inset: 8)
}
}
.environment(\.colorScheme, appState.settings.theme.colorScheme)
@@ -163,7 +165,7 @@ struct MainWindowView: View {
.background {
ZStack {
Color.tokenSurface.opacity(0.94)
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
LinearGradient(
colors: [Color.tokenGreen.opacity(0.085), Color.clear, Color.tokenGreenDark.opacity(0.03)],
startPoint: .leading,
@@ -291,13 +293,13 @@ private struct DashboardTabButton: View {
.frame(height: 28)
.background(
selected
- ? (TokenStepThemeRuntime.isVoyage ? Color.tokenGreen.opacity(0.16) : Color.tokenSurface)
+ ? (TokenStepThemeRuntime.isCinematic ? Color.tokenGreen.opacity(0.16) : Color.tokenSurface)
: Color.clear,
in: RoundedRectangle(cornerRadius: 7, style: .continuous)
)
.overlay(
RoundedRectangle(cornerRadius: 7, style: .continuous)
- .stroke(selected && TokenStepThemeRuntime.isVoyage ? Color.tokenHairlineStrong : Color.clear)
+ .stroke(selected && TokenStepThemeRuntime.isCinematic ? Color.tokenHairlineStrong : Color.clear)
)
.shadow(color: selected ? Color.tokenShadow : .clear, radius: 3, y: 1)
}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverAgentWorkTable.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverAgentWorkTable.swift
index bb736fe..26b7ec4 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverAgentWorkTable.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverAgentWorkTable.swift
@@ -8,7 +8,7 @@ struct PopoverAgentWorkTable: View {
}
var body: some View {
- let voyage = TokenStepThemeRuntime.isVoyage
+ let voyage = TokenStepThemeRuntime.isCinematic
return VStack(alignment: .leading, spacing: 0) {
HStack {
Text(L("Agent 用量"))
@@ -44,7 +44,7 @@ struct PopoverAgentWorkTable: View {
}
private var header: some View {
- let voyage = TokenStepThemeRuntime.isVoyage
+ let voyage = TokenStepThemeRuntime.isCinematic
return HStack(spacing: 10) {
Text(L("来源")).frame(maxWidth: .infinity, alignment: .leading)
Text(L("Token")).frame(width: voyage ? 88 : 72, alignment: .trailing)
@@ -54,7 +54,7 @@ struct PopoverAgentWorkTable: View {
}
private func rowView(_ row: AgentWorkSource) -> some View {
- let voyage = TokenStepThemeRuntime.isVoyage
+ let voyage = TokenStepThemeRuntime.isCinematic
return HStack(alignment: .center, spacing: 10) {
HStack(alignment: .center, spacing: 6) {
Circle()
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverFooterView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverFooterView.swift
index 4e092dc..8aca293 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverFooterView.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverFooterView.swift
@@ -6,7 +6,7 @@ struct PopoverFooterView: View {
var body: some View {
Group {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
odysseyFooter
} else {
classicFooter
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverModelUsageSection.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverModelUsageSection.swift
index c138996..0fabac2 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverModelUsageSection.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverModelUsageSection.swift
@@ -121,7 +121,7 @@ struct PopoverModelUsageSection: View {
}
}
}
- .padding(.horizontal, TokenStepThemeRuntime.isVoyage ? 14 : 12)
+ .padding(.horizontal, TokenStepThemeRuntime.isCinematic ? 14 : 12)
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
@@ -129,7 +129,7 @@ struct PopoverModelUsageSection: View {
private func header(mismatch: Bool) -> some View {
HStack(spacing: 7) {
Text(L("今日模型"))
- .font((TokenStepThemeRuntime.isVoyage ? Font.callout : Font.caption).weight(.heavy))
+ .font((TokenStepThemeRuntime.isCinematic ? Font.callout : Font.caption).weight(.heavy))
.foregroundStyle(Color.tokenInk)
Spacer(minLength: 4)
if mismatch {
@@ -178,7 +178,7 @@ struct PopoverModelUsageSection: View {
.frame(width: 44, alignment: .trailing)
.monospacedDigit()
}
- .font(.system(size: TokenStepThemeRuntime.isVoyage ? 10.5 : 10, weight: .semibold))
+ .font(.system(size: TokenStepThemeRuntime.isCinematic ? 10.5 : 10, weight: .semibold))
.foregroundStyle(Color.tokenInk.opacity(0.82))
GeometryReader { proxy in
@@ -212,7 +212,7 @@ struct PopoverModelUsageSection: View {
guard let slot = row.colorSlot else {
return Color.secondary.opacity(0.72)
}
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
switch slot {
case 0: return .tokenGreen
case 1: return TokenStepThemeRuntime.palette.activity3.color
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverQuotaCard.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverQuotaCard.swift
index 8064312..61c0f42 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverQuotaCard.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverQuotaCard.swift
@@ -4,7 +4,7 @@ struct PopoverQuotaCard: View {
@EnvironmentObject private var appState: AppState
var body: some View {
- let voyage = TokenStepThemeRuntime.isVoyage
+ let voyage = TokenStepThemeRuntime.isCinematic
return VStack(alignment: .leading, spacing: voyage ? 12 : 10) {
HStack(spacing: 8) {
Text(L("订阅额度"))
@@ -57,7 +57,7 @@ struct PopoverQuotaCard: View {
}
private func quotaChip(_ quota: ProviderQuota) -> some View {
- let voyage = TokenStepThemeRuntime.isVoyage
+ let voyage = TokenStepThemeRuntime.isCinematic
return VStack(alignment: .leading, spacing: voyage ? 4 : 6) {
Text(quota.provider.displayName)
.font((voyage ? Font.system(size: 11.5) : Font.caption2).weight(.heavy))
@@ -80,7 +80,7 @@ struct PopoverQuotaCard: View {
}
private func quotaRow(_ window: QuotaWindow) -> some View {
- let voyage = TokenStepThemeRuntime.isVoyage
+ let voyage = TokenStepThemeRuntime.isCinematic
return VStack(alignment: .leading, spacing: voyage ? 3 : 4) {
HStack {
Text(window.kind.shortTitle)
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverTodayRingCard.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverTodayRingCard.swift
index 16d6621..d506433 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverTodayRingCard.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverTodayRingCard.swift
@@ -5,7 +5,7 @@ struct PopoverTodayRingCard: View {
var body: some View {
let lap = appState.todayLap
- let voyage = TokenStepThemeRuntime.isVoyage
+ let voyage = TokenStepThemeRuntime.isCinematic
let ringSize: CGFloat = voyage ? 164 : 118
let lineWidth: CGFloat = voyage ? 14 : 12
@@ -21,12 +21,22 @@ struct PopoverTodayRingCard: View {
}
ZStack {
- if voyage {
+ if TokenStepThemeRuntime.isVoyage {
Circle()
.fill(Color.tokenCanvas.opacity(0.42))
.padding(12)
.blur(radius: 0.2)
VoyageBowProgressView(progress: lap.currentLapProgress, lineWidth: lineWidth, color: lap.ringColor)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ Circle()
+ .fill(Color.black.opacity(0.52))
+ .padding(12)
+ ProgressRingView(progress: lap.currentLapProgress, lineWidth: lineWidth, color: lap.ringColor)
+ .overlay {
+ InterstellarEventHorizonEmblem()
+ .padding(30)
+ .opacity(0.50)
+ }
} else {
ProgressRingView(progress: lap.currentLapProgress, lineWidth: lineWidth, color: lap.ringColor)
}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverTokenRankCard.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverTokenRankCard.swift
index 34117c3..3c17181 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverTokenRankCard.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/PopoverTokenRankCard.swift
@@ -273,21 +273,21 @@ struct PopoverTokenRankCard: View {
VStack(alignment: .leading, spacing: 4) {
Text(L("全榜今日消耗"))
.font(.system(size: 10.5, weight: .heavy))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk.opacity(0.62) : Color.white.opacity(0.62))
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk.opacity(0.62) : Color.white.opacity(0.62))
Text(totalRankTokensText)
.font(.system(size: 22, weight: .heavy, design: .rounded))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk : Color.white)
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk : Color.white)
.monospacedDigit()
if let count = appState.tokenRank?.totalRankedUsers, count > 0 {
Text(LFormat("%d 人参榜", count))
.font(.caption2.weight(.bold))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk.opacity(0.62) : Color.white.opacity(0.62))
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk.opacity(0.62) : Color.white.opacity(0.62))
}
}
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
- TokenStepThemeRuntime.isVoyage
+ TokenStepThemeRuntime.isCinematic
? Color.tokenGreen.opacity(0.11)
: Color(red: 0.12, green: 0.18, blue: 0.14),
in: RoundedRectangle(cornerRadius: 12, style: .continuous)
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/PopoverPanelView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/PopoverPanelView.swift
index 21edd9e..c6c2d5e 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/PopoverPanelView.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/PopoverPanelView.swift
@@ -9,20 +9,22 @@ struct PopoverPanelView: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
header
- if TokenStepThemeRuntime.isVoyage {
- odysseyContent
+ if TokenStepThemeRuntime.isCinematic {
+ cinematicContent
} else {
classicColumns
}
notices
PopoverFooterView()
- .padding(.horizontal, TokenStepThemeRuntime.isVoyage ? 22 : 16)
- .padding(.top, TokenStepThemeRuntime.isVoyage ? 14 : 12)
- .padding(.bottom, TokenStepThemeRuntime.isVoyage ? 20 : 14)
+ .padding(.horizontal, TokenStepThemeRuntime.isCinematic ? 22 : 16)
+ .padding(.top, TokenStepThemeRuntime.isCinematic ? 14 : 12)
+ .padding(.bottom, TokenStepThemeRuntime.isCinematic ? 20 : 14)
}
.frame(width: 900)
.background {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isInterstellar {
+ InterstellarBackdrop(role: .popover, isScreenshotRendering: isScreenshotRendering)
+ } else if TokenStepThemeRuntime.isVoyage {
OdysseyPopoverBackdrop(
isMotionSurfaceActive: odysseyMotionSurfaceVisible,
isScreenshotRendering: isScreenshotRendering
@@ -40,6 +42,8 @@ struct PopoverPanelView: View {
.overlay {
if TokenStepThemeRuntime.isVoyage {
OdysseyPopoverWindowFrame(inset: 7)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarWindowFrame(inset: 7)
}
}
.environment(\.colorScheme, appState.settings.theme.colorScheme)
@@ -57,8 +61,8 @@ struct PopoverPanelView: View {
private var header: some View {
HStack(spacing: 10) {
TokenStepBrandLockup(
- markSize: TokenStepThemeRuntime.isVoyage ? 34 : 28,
- titleSize: TokenStepThemeRuntime.isVoyage ? 20 : 17
+ markSize: TokenStepThemeRuntime.isCinematic ? 34 : 28,
+ titleSize: TokenStepThemeRuntime.isCinematic ? 20 : 17
)
Spacer()
HStack(spacing: 6) {
@@ -86,10 +90,10 @@ struct PopoverPanelView: View {
)
}
}
- .padding(.horizontal, TokenStepThemeRuntime.isVoyage ? 26 : 16)
- .frame(height: TokenStepThemeRuntime.isVoyage ? 72 : 48)
+ .padding(.horizontal, TokenStepThemeRuntime.isCinematic ? 26 : 16)
+ .frame(height: TokenStepThemeRuntime.isCinematic ? 72 : 48)
.background {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
LinearGradient(
colors: [Color.black.opacity(0.30), Color.tokenCanvas.opacity(0.12), Color.black.opacity(0.16)],
startPoint: .leading,
@@ -98,7 +102,7 @@ struct PopoverPanelView: View {
}
}
.overlay(alignment: .bottom) {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
LinearGradient(
colors: [Color.tokenGreen.opacity(0.58), Color.tokenDivider.opacity(0.34), Color.clear],
startPoint: .leading,
@@ -110,23 +114,23 @@ struct PopoverPanelView: View {
}
}
- private var odysseyContent: some View {
+ private var cinematicContent: some View {
VStack(spacing: 10) {
HStack(alignment: .top, spacing: 10) {
PopoverTodayRingCard()
.frame(width: 250, height: odysseyTopCardHeight)
- .background(OdysseyPopoverSectionBackground(opacity: 0.16))
+ .background(cinematicSectionBackground(opacity: TokenStepThemeRuntime.isInterstellar ? 0.66 : 0.16))
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
centralUsageCard
.frame(minWidth: 330, maxWidth: .infinity, minHeight: odysseyTopCardHeight, maxHeight: odysseyTopCardHeight)
- .background(OdysseyPopoverSectionBackground(opacity: 0.46))
+ .background(cinematicSectionBackground(opacity: TokenStepThemeRuntime.isInterstellar ? 0.70 : 0.46))
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
if appState.showsQuotaColumn {
PopoverQuotaCard()
.frame(width: 260, height: odysseyTopCardHeight)
- .background(OdysseyPopoverSectionBackground(opacity: 0.54))
+ .background(cinematicSectionBackground(opacity: TokenStepThemeRuntime.isInterstellar ? 0.74 : 0.54))
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
}
}
@@ -134,7 +138,7 @@ struct PopoverPanelView: View {
if appState.shouldShowAgentWorkRank, appState.agentWorkRankIdentity != nil {
PopoverTokenRankCard(layout: .ribbon)
.frame(height: 76)
- .background(OdysseyPopoverSectionBackground(opacity: 0.58, cornerRadius: 16))
+ .background(cinematicSectionBackground(opacity: TokenStepThemeRuntime.isInterstellar ? 0.76 : 0.58, cornerRadius: 16))
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
@@ -174,6 +178,15 @@ struct PopoverPanelView: View {
private var odysseyTopCardHeight: CGFloat { 288 }
+ @ViewBuilder
+ private func cinematicSectionBackground(opacity: Double, cornerRadius: CGFloat = 18) -> some View {
+ if TokenStepThemeRuntime.isInterstellar {
+ InterstellarPanelBackground(opacity: opacity, cornerRadius: cornerRadius)
+ } else {
+ OdysseyPopoverSectionBackground(opacity: opacity, cornerRadius: cornerRadius)
+ }
+ }
+
private var showsModelUsageSection: Bool {
switch PopoverModelUsageRows.state(from: appState.today) {
case .hidden: return false
@@ -182,7 +195,7 @@ struct PopoverPanelView: View {
}
private var centralUsageCard: some View {
- let voyage = TokenStepThemeRuntime.isVoyage
+ let voyage = TokenStepThemeRuntime.isCinematic
return VStack(spacing: 0) {
PopoverAgentWorkTable()
.frame(height: voyage ? (showsModelUsageSection ? 128 : odysseyTopCardHeight) : nil)
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/PrivacyView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/PrivacyView.swift
index c6060fc..00b9de7 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/PrivacyView.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/PrivacyView.swift
@@ -24,6 +24,11 @@ struct PrivacyView: View {
.frame(width: 132, height: 104)
.padding(14)
.opacity(0.48)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarEventHorizonEmblem()
+ .frame(width: 150, height: 78)
+ .padding(18)
+ .opacity(0.44)
}
}
@@ -67,7 +72,7 @@ struct PrivacyView: View {
}
.overlay(
RoundedRectangle(cornerRadius: 24, style: .continuous)
- .stroke(TokenStepThemeRuntime.isVoyage ? Color.tokenHairlineStrong : Color(red: 0.42, green: 0.36, blue: 0.82).opacity(0.16))
+ .stroke(TokenStepThemeRuntime.isCinematic ? Color.tokenHairlineStrong : Color(red: 0.42, green: 0.36, blue: 0.82).opacity(0.16))
)
}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/ScreenshotCaptureViews.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/ScreenshotCaptureViews.swift
index 36881a2..79914e3 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/ScreenshotCaptureViews.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/ScreenshotCaptureViews.swift
@@ -16,6 +16,8 @@ struct DashboardScreenshotView: View {
if TokenStepThemeRuntime.isVoyage {
VoyageWindowFrame(inset: 8)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarWindowFrame(inset: 8)
}
}
.frame(width: 1000)
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Settings/SettingsControls.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Settings/SettingsControls.swift
index 8be510b..ee0f28a 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Settings/SettingsControls.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Settings/SettingsControls.swift
@@ -35,7 +35,7 @@ enum SettingsBadgeStyle {
case ok, warn, off, l1, l2, l3
var foreground: Color {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
switch self {
case .ok, .l1: return Color.tokenSuccess
case .warn: return Color.tokenWarning
@@ -54,7 +54,7 @@ enum SettingsBadgeStyle {
}
var background: Color {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
switch self {
case .ok, .l1: return Color.tokenSuccess.opacity(0.12)
case .warn: return Color.tokenWarning.opacity(0.12)
@@ -73,7 +73,7 @@ enum SettingsBadgeStyle {
}
var border: Color {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
switch self {
case .ok, .l1: return Color.tokenSuccess.opacity(0.30)
case .warn: return Color.tokenWarning.opacity(0.34)
@@ -163,7 +163,7 @@ struct SettingsSectionCard: View {
}
private var sectionBackground: Color {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
return Color.tokenSurface
}
switch tint ?? badgeStyle {
@@ -174,7 +174,7 @@ struct SettingsSectionCard: View {
}
private var sectionBorder: Color {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
switch tint ?? badgeStyle {
case .l2: return Color.tokenGreen.opacity(0.30)
case .l3: return Color.tokenGreenDark.opacity(0.34)
@@ -258,7 +258,7 @@ struct SettingsCard: View {
ZStack {
RoundedRectangle(cornerRadius: 24, style: .continuous)
.fill(Color.tokenSurface)
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
LinearGradient(
colors: [Color.tokenGreen.opacity(0.05), Color.clear, Color.tokenGreenDark.opacity(0.025)],
startPoint: .topLeading,
@@ -273,6 +273,8 @@ struct SettingsCard: View {
RoundedRectangle(cornerRadius: 24, style: .continuous).stroke(Color.tokenHairline)
if TokenStepThemeRuntime.isVoyage {
VoyageCardOrnament(cornerRadius: 24)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarCardOrnament(cornerRadius: 24)
}
}
}
@@ -306,7 +308,7 @@ struct ThemeSwatchButton: View {
Button(action: action) {
VStack(spacing: 7) {
ZStack {
- if theme == .voyage {
+ if theme == .voyage || theme == .eventHorizon {
TokenStepMark(size: 38)
.shadow(color: theme.palette.accentDark.color.opacity(selected ? 0.24 : 0.12), radius: 8, x: 0, y: 4)
} else {
@@ -329,7 +331,7 @@ struct ThemeSwatchButton: View {
if selected && theme != .voyage {
Image(systemName: "checkmark")
.font(.system(size: 13, weight: .heavy))
- .foregroundStyle(theme == .voyage ? Color.tokenActionText : Color.white)
+ .foregroundStyle(theme == .voyage || theme == .eventHorizon ? Color.tokenActionText : Color.white)
}
}
@@ -442,6 +444,8 @@ struct ThemePackOptionButton: View {
}
}
.overlay(RoundedRectangle(cornerRadius: 13, style: .continuous).stroke(odysseyChapter.palette.accent.color.opacity(0.46)))
+ case .interstellar:
+ InterstellarThemePreview()
}
}
}
@@ -608,7 +612,7 @@ struct TokenStepSwitchToggleStyle: ToggleStyle {
.fill(
configuration.isOn
? Color.tokenToggleTint
- : Color.tokenTrack.opacity(TokenStepThemeRuntime.isVoyage ? 0.82 : 0.72)
+ : Color.tokenTrack.opacity(TokenStepThemeRuntime.isCinematic ? 0.82 : 0.72)
)
Circle()
.fill(configuration.isOn ? Color.tokenInk : Color.tokenInk.opacity(0.62))
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Settings/SettingsGeneralPane.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Settings/SettingsGeneralPane.swift
index 17ab397..14550e8 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Settings/SettingsGeneralPane.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Settings/SettingsGeneralPane.swift
@@ -66,7 +66,7 @@ struct SettingsGeneralPane: View {
SettingsSectionCard(
title: L("主题皮肤包"),
- subtitle: L("经典配色与奥德赛视觉篇章可随时切换")
+ subtitle: L("经典、奥德赛与引力边界可随时切换")
) {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 10) {
@@ -123,6 +123,32 @@ struct SettingsGeneralPane: View {
}
}
.transition(.opacity.combined(with: .move(edge: .top)))
+ } else if appState.settings.themePack == .interstellar {
+ HStack(spacing: 14) {
+ InterstellarEventHorizonEmblem()
+ .frame(width: 126, height: 58)
+
+ VStack(alignment: .leading, spacing: 3) {
+ Text(L("引力边界 · 近距事件视界"))
+ .font(.callout.weight(.heavy))
+ .foregroundStyle(Color.tokenInk)
+ Text(L("巨大黑洞、象牙白吸积盘与克制电影色;低亮与减少动态时自动静止。"))
+ .font(.caption.weight(.semibold))
+ .foregroundStyle(.secondary)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+
+ Spacer(minLength: 8)
+
+ Text(L("原创动态主题"))
+ .font(.caption.weight(.heavy))
+ .foregroundStyle(Color.tokenGreenDark)
+ .padding(.horizontal, 9)
+ .padding(.vertical, 5)
+ .background(Color.tokenGreen.opacity(0.12), in: Capsule())
+ .overlay(Capsule().stroke(Color.tokenHairlineStrong))
+ }
+ .transition(.opacity.combined(with: .move(edge: .top)))
} else {
HStack {
VStack(alignment: .leading, spacing: 2) {
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/SettingsView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/SettingsView.swift
index c81a716..579615e 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/SettingsView.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/SettingsView.swift
@@ -62,6 +62,8 @@ struct SettingsView: View {
.overlay {
if TokenStepThemeRuntime.isVoyage {
VoyageWindowFrame(inset: 8)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarWindowFrame(inset: 8)
}
}
}
@@ -140,6 +142,11 @@ struct SettingsView: View {
.frame(width: 124, height: 72)
.opacity(0.38)
.offset(x: -92, y: 3)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarEventHorizonEmblem()
+ .frame(width: 136, height: 58)
+ .opacity(0.42)
+ .offset(x: -92, y: 3)
}
}
}
@@ -246,13 +253,13 @@ private struct DashboardSettingsTab: View {
.frame(height: 28)
.background(
selected
- ? (TokenStepThemeRuntime.isVoyage ? Color.tokenGreen.opacity(0.16) : Color.tokenSurface)
+ ? (TokenStepThemeRuntime.isCinematic ? Color.tokenGreen.opacity(0.16) : Color.tokenSurface)
: Color.clear,
in: RoundedRectangle(cornerRadius: 7, style: .continuous)
)
.overlay(
RoundedRectangle(cornerRadius: 7, style: .continuous)
- .stroke(selected && TokenStepThemeRuntime.isVoyage ? Color.tokenHairlineStrong : Color.clear)
+ .stroke(selected && TokenStepThemeRuntime.isCinematic ? Color.tokenHairlineStrong : Color.clear)
)
.shadow(color: selected ? Color.tokenShadow : .clear, radius: 3, y: 1)
}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Share/ShareDailyCardView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Share/ShareDailyCardView.swift
index 6ce30a0..0a6f5e4 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Share/ShareDailyCardView.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Share/ShareDailyCardView.swift
@@ -63,6 +63,8 @@ struct ShareDailyCardView: View {
if TokenStepThemeRuntime.isVoyage {
VoyageWindowFrame(inset: 8)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarWindowFrame(inset: 8)
}
}
.frame(width: 600, height: 980)
@@ -94,6 +96,11 @@ struct ShareDailyCardView: View {
.frame(width: 230, height: 210)
.opacity(0.24)
.offset(x: 18, y: 12)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarEventHorizonEmblem()
+ .frame(width: 250, height: 150)
+ .opacity(0.34)
+ .offset(x: 18, y: 12)
}
HStack(alignment: .center, spacing: 18) {
@@ -105,6 +112,13 @@ struct ShareDailyCardView: View {
Group {
if TokenStepThemeRuntime.isVoyage {
VoyageBowProgressView(progress: lap.currentLapProgress, lineWidth: 20, color: lap.color)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ ProgressRingView(progress: lap.currentLapProgress, lineWidth: 20, color: lap.color)
+ .overlay {
+ InterstellarEventHorizonEmblem()
+ .padding(42)
+ .opacity(0.52)
+ }
} else {
ProgressRingView(progress: lap.currentLapProgress, lineWidth: 20, color: lap.color)
}
@@ -445,7 +459,7 @@ private struct ShareCardSurface: View {
.background {
ZStack {
shape.fill(Color.tokenSurface)
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
LinearGradient(
colors: [Color.tokenGreen.opacity(0.055), Color.clear, Color.tokenGreenDark.opacity(0.025)],
startPoint: .topLeading,
@@ -460,6 +474,8 @@ private struct ShareCardSurface: View {
shape.stroke(Color.tokenHairline)
if TokenStepThemeRuntime.isVoyage {
VoyageCardOrnament(cornerRadius: cornerRadius)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarCardOrnament(cornerRadius: cornerRadius)
}
}
}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Share/ShareRhythmCardView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Share/ShareRhythmCardView.swift
index d329fe9..b763e2b 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/Share/ShareRhythmCardView.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Share/ShareRhythmCardView.swift
@@ -18,6 +18,9 @@ struct ShareRhythmCardView: View {
if TokenStepThemeRuntime.isVoyage {
VoyageBackdropAtmosphere(role: .share)
.opacity(0.72)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarBackdrop(role: .share, isScreenshotRendering: true)
+ .opacity(0.88)
}
VStack(spacing: 14) {
@@ -35,6 +38,8 @@ struct ShareRhythmCardView: View {
if TokenStepThemeRuntime.isVoyage {
VoyageWindowFrame(inset: 8)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarWindowFrame(inset: 8)
}
}
.frame(width: 600, height: 840)
@@ -57,7 +62,7 @@ struct ShareRhythmCardView: View {
.foregroundStyle(palette.accent)
Text(weekdayText)
.font(.callout.weight(.bold))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk.opacity(0.56) : Color.white.opacity(0.56))
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk.opacity(0.56) : Color.white.opacity(0.56))
}
}
}
@@ -66,7 +71,7 @@ struct ShareRhythmCardView: View {
VStack(spacing: 8) {
Text(L("昨日 AI 节奏"))
.font(.system(size: 31, weight: .black, design: .rounded))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk : Color.white)
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk : Color.white)
.lineLimit(1)
HStack(alignment: .center, spacing: 12) {
@@ -92,7 +97,7 @@ struct ShareRhythmCardView: View {
Text(rhythm.primaryTag.shareLine)
.font(.callout.weight(.bold))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk.opacity(0.66) : Color.white.opacity(0.66))
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk.opacity(0.66) : Color.white.opacity(0.66))
.lineLimit(1)
.minimumScaleFactor(0.76)
}
@@ -108,12 +113,12 @@ struct ShareRhythmCardView: View {
.background(palette.accent.opacity(0.12), in: Circle())
Text(LFormat("峰值 %@", peakWindowText))
.font(.headline.weight(.black))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk : Color.white)
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk : Color.white)
.lineLimit(1)
Spacer()
Text(LFormat("峰值 %@", TokenStepFormat.tokens(rhythm.peakTokens, compact: true)))
.font(.callout.weight(.heavy))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk.opacity(0.72) : Color.white.opacity(0.72))
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk.opacity(0.72) : Color.white.opacity(0.72))
.lineLimit(1)
}
.padding(.horizontal, 18)
@@ -137,12 +142,12 @@ struct ShareRhythmCardView: View {
VStack(spacing: 3) {
Text(L("昨日 Token"))
.font(.headline.weight(.black))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk.opacity(0.70) : Color.white.opacity(0.70))
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk.opacity(0.70) : Color.white.opacity(0.70))
Text(TokenStepFormat.tokens(day.totalTokens, compact: true))
.font(.system(size: 49, weight: .black, design: .rounded))
.foregroundStyle(
LinearGradient(
- colors: [palette.accent, TokenStepThemeRuntime.isVoyage ? Color.tokenInk : Color.white],
+ colors: [palette.accent, TokenStepThemeRuntime.isCinematic ? Color.tokenInk : Color.white],
startPoint: .top,
endPoint: .bottom
)
@@ -185,7 +190,7 @@ struct ShareRhythmCardView: View {
Text(L("不上传对话"))
}
.font(.caption.weight(.heavy))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk.opacity(0.54) : Color.white.opacity(0.54))
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk.opacity(0.54) : Color.white.opacity(0.54))
.frame(maxWidth: .infinity)
.frame(height: 38)
.background(Color.black.opacity(0.20), in: Capsule())
@@ -314,7 +319,7 @@ private struct RhythmAxisLabel: View {
VStack(spacing: 4) {
Text(AgentWorkCopy.hourLabel(hour))
.font(.caption.weight(.heavy))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk.opacity(0.48) : Color.white.opacity(0.48))
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk.opacity(0.48) : Color.white.opacity(0.48))
if let symbol {
Image(systemName: symbol)
.font(.system(size: 16, weight: .bold))
@@ -344,7 +349,7 @@ private struct RhythmBottomMetric: View {
Text(value)
.font(.system(size: 24, weight: .black, design: .rounded))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk : Color.white)
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk : Color.white)
.minimumScaleFactor(0.68)
.lineLimit(1)
}
@@ -392,7 +397,7 @@ private struct RhythmPeakMarker: View {
.stroke(color.opacity(0.36), style: StrokeStyle(lineWidth: 1.5, dash: [5, 7]))
Circle()
- .fill(TokenStepThemeRuntime.isVoyage ? Color.tokenInk : Color.white)
+ .fill(TokenStepThemeRuntime.isCinematic ? Color.tokenInk : Color.white)
.frame(width: 11, height: 11)
.overlay(Circle().stroke(color, lineWidth: 3))
.shadow(color: color.opacity(0.8), radius: 14, x: 0, y: 0)
@@ -564,7 +569,7 @@ private struct RhythmCardBackdrop: View {
.rotationEffect(.degrees(-23))
.offset(x: 250, y: -48)
RhythmGridShape(columns: 11, rows: 14)
- .stroke(TokenStepThemeRuntime.isVoyage ? Color.tokenInk.opacity(0.026) : Color.white.opacity(0.026), lineWidth: 1)
+ .stroke(TokenStepThemeRuntime.isCinematic ? Color.tokenInk.opacity(0.026) : Color.white.opacity(0.026), lineWidth: 1)
}
}
}
@@ -577,7 +582,7 @@ private struct RhythmCardPalette {
var panel: Color
static func palette(for rhythm: DailyRhythm) -> RhythmCardPalette {
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
return RhythmCardPalette(
background: [
Color(red: 9 / 255, green: 10 / 255, blue: 9 / 255),
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/TodayDashboardCards.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/TodayDashboardCards.swift
index f5bb8cf..4fb0087 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/TodayDashboardCards.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/TodayDashboardCards.swift
@@ -10,6 +10,13 @@ struct TodayHeroCard: View {
ZStack {
if TokenStepThemeRuntime.isVoyage {
VoyageBowProgressView(progress: lap.currentLapProgress, lineWidth: 16, color: lap.ringColor)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ ProgressRingView(progress: lap.currentLapProgress, lineWidth: 16, color: lap.ringColor)
+ .overlay {
+ InterstellarEventHorizonEmblem()
+ .padding(34)
+ .opacity(0.50)
+ }
} else {
ProgressRingView(progress: lap.currentLapProgress, lineWidth: 16, color: lap.ringColor)
}
@@ -93,6 +100,11 @@ struct TodayAgentIntensityCard: View {
.frame(width: 94, height: 76)
.padding(14)
.opacity(0.54)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarEventHorizonEmblem()
+ .frame(width: 116, height: 58)
+ .padding(14)
+ .opacity(0.44)
}
}
}
@@ -172,6 +184,11 @@ struct TodayHourlyCard: View {
.frame(width: 180, height: 86)
.padding(12)
.opacity(0.46)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarEventHorizonEmblem()
+ .frame(width: 180, height: 70)
+ .padding(12)
+ .opacity(0.34)
}
}
}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/TokenIslandView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/TokenIslandView.swift
index 057fa7f..9249cd8 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/TokenIslandView.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/TokenIslandView.swift
@@ -90,7 +90,7 @@ struct TokenIslandRingView: View {
Text(TokenStepFormat.tokens(tokens, compact: true, language: language))
.font(.system(size: 13, weight: .heavy, design: .rounded))
- .foregroundStyle(TokenStepThemeRuntime.isVoyage ? Color.tokenInk : Color.white)
+ .foregroundStyle(TokenStepThemeRuntime.isCinematic ? Color.tokenInk : Color.white)
.monospacedDigit()
.lineLimit(1)
.minimumScaleFactor(0.74)
@@ -101,18 +101,23 @@ struct TokenIslandRingView: View {
.background {
ZStack {
Color.black
- if TokenStepThemeRuntime.isVoyage {
+ if TokenStepThemeRuntime.isCinematic {
LinearGradient(
colors: [Color.tokenCanvas, Color.tokenGreen.opacity(0.16), Color.tokenCanvas],
startPoint: .leading,
endPoint: .trailing
)
}
+ if TokenStepThemeRuntime.isInterstellar {
+ InterstellarEventHorizonEmblem()
+ .padding(.horizontal, 5)
+ .opacity(0.42)
+ }
}
}
.clipShape(Capsule())
- .overlay(Capsule().stroke(TokenStepThemeRuntime.isVoyage ? Color.tokenHairlineStrong : Color.clear))
- .shadow(color: TokenStepThemeRuntime.isVoyage ? Color.tokenGreen.opacity(0.16) : .clear, radius: 8)
+ .overlay(Capsule().stroke(TokenStepThemeRuntime.isCinematic ? Color.tokenHairlineStrong : Color.clear))
+ .shadow(color: TokenStepThemeRuntime.isCinematic ? Color.tokenGreen.opacity(0.16) : .clear, radius: 8)
.id("\(theme.id)-\(language.resolved.id)")
}
}
@@ -191,8 +196,15 @@ private struct TokenIslandExpandedView: View {
private var ring: some View {
ZStack {
- if TokenStepThemeRuntime.theme == .voyage {
+ if TokenStepThemeRuntime.isVoyage {
VoyageBowProgressView(progress: lap.currentLapProgress, lineWidth: 9, color: lap.ringColor)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ ProgressRingView(progress: lap.currentLapProgress, lineWidth: 9, color: lap.ringColor)
+ .overlay {
+ InterstellarEventHorizonEmblem()
+ .padding(18)
+ .opacity(0.48)
+ }
} else {
ProgressRingView(progress: lap.currentLapProgress, lineWidth: 9, color: lap.ringColor)
}
@@ -236,6 +248,8 @@ private struct TokenIslandExpandedSurface: View {
shape.stroke(Color.tokenHairlineStrong)
if TokenStepThemeRuntime.isVoyage {
VoyageCardOrnament(cornerRadius: TokenIslandMetrics.expandedCornerRadius)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarCardOrnament(cornerRadius: TokenIslandMetrics.expandedCornerRadius)
}
}
}
@@ -259,6 +273,8 @@ private struct TokenIslandExpandedBackground: View {
)
if TokenStepThemeRuntime.isVoyage {
VoyageBackdropAtmosphere(role: .island)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarBackdrop(role: .island)
}
}
}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/UpdateWindowView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/UpdateWindowView.swift
index a73233a..c08d8c1 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/UpdateWindowView.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/UpdateWindowView.swift
@@ -21,6 +21,8 @@ struct UpdateWindowView: View {
.overlay {
if TokenStepThemeRuntime.isVoyage {
VoyageWindowFrame(inset: 8)
+ } else if TokenStepThemeRuntime.isInterstellar {
+ InterstellarWindowFrame(inset: 8)
}
}
.environment(\.colorScheme, appState.settings.theme.colorScheme)
@@ -122,6 +124,14 @@ struct UpdateWindowView: View {
}
.frame(height: 8)
}
+ .overlay(alignment: .trailing) {
+ if TokenStepThemeRuntime.isInterstellar {
+ InterstellarEventHorizonEmblem()
+ .frame(width: 72, height: 24)
+ .offset(y: 12)
+ .opacity(0.55)
+ }
+ }
}
}
diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/VoyageSkin.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/VoyageSkin.swift
index 5ce6d6a..00902da 100644
--- a/TokenStepSwift/Sources/TokenStepSwift/Views/VoyageSkin.swift
+++ b/TokenStepSwift/Sources/TokenStepSwift/Views/VoyageSkin.swift
@@ -259,18 +259,22 @@ struct TokenStepBrandLockup: View {
HStack(spacing: max(8, markSize * 0.30)) {
TokenStepMark(size: markSize)
- VStack(alignment: .leading, spacing: TokenStepThemeRuntime.isVoyage && showsEdition ? 1 : 0) {
+ VStack(alignment: .leading, spacing: TokenStepThemeRuntime.isCinematic && showsEdition ? 1 : 0) {
Text("TokenStep")
.font(.system(size: titleSize, weight: .heavy, design: .rounded))
.foregroundStyle(Color.tokenInk)
- .tracking(TokenStepThemeRuntime.isVoyage ? 0.2 : 0)
+ .tracking(TokenStepThemeRuntime.isCinematic ? 0.2 : 0)
- if TokenStepThemeRuntime.isVoyage && showsEdition {
+ if TokenStepThemeRuntime.isCinematic && showsEdition {
HStack(spacing: 5) {
Rectangle()
.fill(Color.tokenGreen)
.frame(width: 12, height: 1)
- Text(TokenStepThemeRuntime.odysseyChapter.editionLabel)
+ Text(
+ TokenStepThemeRuntime.isInterstellar
+ ? "GRAVITY · EVENT HORIZON"
+ : TokenStepThemeRuntime.odysseyChapter.editionLabel
+ )
.font(.system(size: max(7, titleSize * 0.47), weight: .bold, design: .serif))
.tracking(1.35)
.foregroundStyle(Color.tokenGreenDark.opacity(0.84))
diff --git a/TokenStepSwift/Tests/Fixtures/PopoverPanelRender.swift b/TokenStepSwift/Tests/Fixtures/PopoverPanelRender.swift
index 795a401..24ed94b 100644
--- a/TokenStepSwift/Tests/Fixtures/PopoverPanelRender.swift
+++ b/TokenStepSwift/Tests/Fixtures/PopoverPanelRender.swift
@@ -13,9 +13,12 @@ struct PopoverPanelRender {
NSApp.applicationIconImage = icon
}
- let theme = ProcessInfo.processInfo.environment["TOKENSTEP_POPOVER_THEME"] == "voyage"
- ? TokenStepTheme.voyage
- : TokenStepTheme.green
+ let theme: TokenStepTheme
+ switch ProcessInfo.processInfo.environment["TOKENSTEP_POPOVER_THEME"] {
+ case "voyage": theme = .voyage
+ case "event_horizon": theme = .eventHorizon
+ default: theme = .green
+ }
let chapter = ProcessInfo.processInfo.environment["TOKENSTEP_ODYSSEY_CHAPTER"]
.flatMap(TokenStepOdysseyChapter.init(rawValue:))
?? .aegeanMist
@@ -47,7 +50,7 @@ struct PopoverPanelRender {
let output = URL(
fileURLWithPath: ProcessInfo.processInfo.environment["TOKENSTEP_POPOVER_RENDER_PATH"]
- ?? "/tmp/tokenstep-popover-v0.2.4.png"
+ ?? "/tmp/tokenstep-popover-v0.2.7.png"
)
try FileManager.default.createDirectory(
at: output.deletingLastPathComponent(),
@@ -75,7 +78,7 @@ struct PopoverPanelRender {
NSLocalizedDescriptionKey: "Unexpected render size: \(bitmap.pixelsWide)x\(bitmap.pixelsHigh)"
])
}
- print("v0.2.5-a2: \(bitmap.pixelsWide)x\(bitmap.pixelsHigh) -> \(output.path)")
+ print("v0.2.7: \(bitmap.pixelsWide)x\(bitmap.pixelsHigh) -> \(output.path)")
}
private static func validateIsolatedAppSupport() throws {
diff --git a/TokenStepSwift/Tests/Fixtures/VoyageInterfaceRender.swift b/TokenStepSwift/Tests/Fixtures/VoyageInterfaceRender.swift
index cefd423..62ce4bd 100644
--- a/TokenStepSwift/Tests/Fixtures/VoyageInterfaceRender.swift
+++ b/TokenStepSwift/Tests/Fixtures/VoyageInterfaceRender.swift
@@ -14,9 +14,12 @@ struct VoyageInterfaceRender {
let chapter = ProcessInfo.processInfo.environment["TOKENSTEP_ODYSSEY_CHAPTER"]
.flatMap(TokenStepOdysseyChapter.init(rawValue:))
?? .directorsCut
- let theme: TokenStepTheme = ProcessInfo.processInfo.environment["TOKENSTEP_THEME_PACK"] == "classic"
- ? .green
- : .voyage
+ let theme: TokenStepTheme
+ switch ProcessInfo.processInfo.environment["TOKENSTEP_THEME_PACK"] {
+ case "classic": theme = .green
+ case "interstellar": theme = .eventHorizon
+ default: theme = .voyage
+ }
let settings = fixtureSettings(theme: theme, chapter: chapter)
renderColorScheme = settings.theme.colorScheme
try writeJSON(snapshot, to: AppPaths.usageJSON)
diff --git a/TokenStepSwift/Tests/TokenStepSwiftTests/InterstellarMotionTests.swift b/TokenStepSwift/Tests/TokenStepSwiftTests/InterstellarMotionTests.swift
new file mode 100644
index 0000000..cea12ba
--- /dev/null
+++ b/TokenStepSwift/Tests/TokenStepSwiftTests/InterstellarMotionTests.swift
@@ -0,0 +1,36 @@
+import XCTest
+@testable import TokenStepSwift
+
+final class InterstellarMotionTests: XCTestCase {
+ func testMotionRunsAtLowCostOnlyOnVisibleInteractiveSurfaces() {
+ XCTAssertEqual(InterstellarMotionPolicy.preferredFramesPerSecond, 24)
+ XCTAssertTrue(
+ InterstellarMotionPolicy.shouldAnimate(
+ reduceMotion: false,
+ isScreenshotRendering: false,
+ isControlActive: true
+ )
+ )
+ XCTAssertFalse(
+ InterstellarMotionPolicy.shouldAnimate(
+ reduceMotion: true,
+ isScreenshotRendering: false,
+ isControlActive: true
+ )
+ )
+ XCTAssertFalse(
+ InterstellarMotionPolicy.shouldAnimate(
+ reduceMotion: false,
+ isScreenshotRendering: true,
+ isControlActive: true
+ )
+ )
+ XCTAssertFalse(
+ InterstellarMotionPolicy.shouldAnimate(
+ reduceMotion: false,
+ isScreenshotRendering: false,
+ isControlActive: false
+ )
+ )
+ }
+}
diff --git a/TokenStepSwift/Tests/TokenStepSwiftTests/SettingsMigrationTests.swift b/TokenStepSwift/Tests/TokenStepSwiftTests/SettingsMigrationTests.swift
index 6d0aca6..0becf1f 100644
--- a/TokenStepSwift/Tests/TokenStepSwiftTests/SettingsMigrationTests.swift
+++ b/TokenStepSwift/Tests/TokenStepSwiftTests/SettingsMigrationTests.swift
@@ -104,6 +104,22 @@ final class SettingsMigrationTests: XCTestCase {
XCTAssertEqual(decoded.odysseyChapter, .ashMarble)
}
+ func testInterstellarThemeRoundTripsAsDedicatedThemePack() throws {
+ var settings = TokenStepSettings.defaults
+ settings.theme = .eventHorizon
+ settings.classicTheme = .ocean
+
+ let decoded = try JSONDecoder().decode(
+ TokenStepSettings.self,
+ from: JSONEncoder().encode(settings)
+ )
+
+ XCTAssertEqual(decoded.theme, .eventHorizon)
+ XCTAssertEqual(decoded.themePack, .interstellar)
+ XCTAssertEqual(decoded.classicTheme, .ocean)
+ XCTAssertEqual(decoded.activeThemeTitle, L("引力边界"))
+ }
+
func testCursorFlagStaysInSyncWithProviderSet() {
var settings = TokenStepSettings.defaults
settings.setQuotaProvider(.cursor, enabled: true)
diff --git a/TokenStepSwift/Tests/TokenStepSwiftTests/UpdateServiceTests.swift b/TokenStepSwift/Tests/TokenStepSwiftTests/UpdateServiceTests.swift
index 903385c..c209b23 100644
--- a/TokenStepSwift/Tests/TokenStepSwiftTests/UpdateServiceTests.swift
+++ b/TokenStepSwift/Tests/TokenStepSwiftTests/UpdateServiceTests.swift
@@ -103,13 +103,21 @@ final class UpdateServiceTests: XCTestCase {
}
}
- func testManualCheckBypassesToggleThrottleAndSkippedVersion() {
+ func testManualPolicyBypassesToggleAndAutomaticThrottleAndSkippedVersion() {
let now = Date(timeIntervalSince1970: 100_000)
XCTAssertTrue(
UpdateCheckPolicy.shouldStart(
trigger: .manual,
autoUpdateEnabled: false,
- lastAttemptAt: now,
+ lastAttemptAt: now.addingTimeInterval(-10),
+ now: now
+ )
+ )
+ XCTAssertTrue(
+ UpdateCheckPolicy.shouldStart(
+ trigger: .manual,
+ autoUpdateEnabled: false,
+ lastAttemptAt: now.addingTimeInterval(-9),
now: now
)
)
@@ -234,13 +242,120 @@ final class UpdateServiceTests: XCTestCase {
)
)
gate.finish()
- XCTAssertTrue(
+ XCTAssertFalse(
gate.begin(
trigger: .manual,
autoUpdateEnabled: false,
now: now.addingTimeInterval(2)
)
)
+ XCTAssertTrue(
+ gate.begin(
+ trigger: .manual,
+ autoUpdateEnabled: false,
+ now: now.addingTimeInterval(10)
+ )
+ )
+ }
+
+ func testRecentAutomaticCheckDoesNotBlockManualCheck() {
+ let now = Date(timeIntervalSince1970: 100_000)
+ var gate = UpdateCheckGate()
+ XCTAssertTrue(
+ gate.begin(
+ trigger: .startup,
+ autoUpdateEnabled: true,
+ now: now
+ )
+ )
+ gate.finish()
+ XCTAssertTrue(
+ gate.begin(
+ trigger: .manual,
+ autoUpdateEnabled: true,
+ now: now.addingTimeInterval(1)
+ )
+ )
+ }
+
+ func testFallbackLatestRedirectBuildsVersionedDMGWithoutGitHubAPI() throws {
+ let result = try UpdateService.evaluateFallbackReleaseResponse(
+ responseURL: URL(string: "https://github.com/Backtthefuture/TokenStep/releases/tag/v0.2.7"),
+ statusCode: 200,
+ currentVersion: "0.2.6"
+ )
+
+ guard case let .available(update) = result else {
+ return XCTFail("Expected fallback release to be available")
+ }
+ XCTAssertEqual(update.version, "0.2.7")
+ XCTAssertEqual(update.assetName, "TokenStep-0.2.7.dmg")
+ XCTAssertEqual(
+ update.assetURL.absoluteString,
+ "https://github.com/Backtthefuture/TokenStep/releases/download/v0.2.7/TokenStep-0.2.7.dmg"
+ )
+ XCTAssertEqual(update.formattedSize, L("发布包"))
+ }
+
+ func testFallbackLatestRedirectRejectsUnresolvedLatestPage() {
+ XCTAssertThrowsError(
+ try UpdateService.evaluateFallbackReleaseResponse(
+ responseURL: URL(string: "https://github.com/Backtthefuture/TokenStep/releases/latest"),
+ statusCode: 200,
+ currentVersion: "0.2.6"
+ )
+ ) { error in
+ XCTAssertEqual(error as? UpdateError, .checkFailed)
+ }
+ }
+
+ func testRateLimitedAPIUsesNonAPIFallback() async throws {
+ let configuration = URLSessionConfiguration.ephemeral
+ configuration.protocolClasses = [UpdateMockURLProtocol.self]
+ let session = URLSession(configuration: configuration)
+ UpdateMockURLProtocol.handler = { request in
+ if request.url?.host == "api.example.com" {
+ let response = HTTPURLResponse(
+ url: request.url!,
+ statusCode: 403,
+ httpVersion: nil,
+ headerFields: [
+ "X-RateLimit-Remaining": "0",
+ "X-RateLimit-Reset": "100000"
+ ]
+ )!
+ return (response, Data())
+ }
+
+ let resolvedURL = URL(string: "https://github.com/Backtthefuture/TokenStep/releases/tag/v0.2.7")!
+ let response = HTTPURLResponse(
+ url: resolvedURL,
+ statusCode: 200,
+ httpVersion: nil,
+ headerFields: ["Content-Type": "text/html"]
+ )!
+ return (response, Data())
+ }
+ defer { UpdateMockURLProtocol.handler = nil }
+
+ let result = try await UpdateService.checkForUpdates(
+ currentVersion: "0.2.6",
+ session: session,
+ releaseURL: URL(string: "https://api.example.com/releases/latest")!,
+ fallbackURL: URL(string: "https://www.example.com/releases/latest")!
+ )
+
+ guard case let .available(update) = result else {
+ return XCTFail("Expected fallback update")
+ }
+ XCTAssertEqual(update.version, "0.2.7")
+ }
+
+ func testRateLimitErrorExplainsRecoveryTime() {
+ let reset = Date(timeIntervalSince1970: 100_000)
+ let message = UpdateError.rateLimited(resetAt: reset).localizedDescription
+ XCTAssertTrue(message.contains("GitHub"))
+ XCTAssertTrue(message.contains(":"))
}
private func releaseData(
@@ -287,3 +402,27 @@ private extension String {
hasPrefix("v") ? String(dropFirst()) : self
}
}
+
+private final class UpdateMockURLProtocol: URLProtocol {
+ static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
+
+ override class func canInit(with request: URLRequest) -> Bool { true }
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
+
+ override func startLoading() {
+ guard let handler = Self.handler else {
+ client?.urlProtocol(self, didFailWithError: UpdateError.checkFailed)
+ return
+ }
+ do {
+ let (response, data) = try handler(request)
+ client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ client?.urlProtocol(self, didLoad: data)
+ client?.urlProtocolDidFinishLoading(self)
+ } catch {
+ client?.urlProtocol(self, didFailWithError: error)
+ }
+ }
+
+ override func stopLoading() {}
+}
diff --git a/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonHero.png b/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonHero.png
new file mode 100644
index 0000000..ec44e8c
Binary files /dev/null and b/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonHero.png differ
diff --git a/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonQuiet.png b/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonQuiet.png
new file mode 100644
index 0000000..3361aa1
Binary files /dev/null and b/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonQuiet.png differ
diff --git a/TokenUsageMenuApp/assets/interstellar/README.md b/TokenUsageMenuApp/assets/interstellar/README.md
new file mode 100644
index 0000000..4800794
--- /dev/null
+++ b/TokenUsageMenuApp/assets/interstellar/README.md
@@ -0,0 +1,8 @@
+# Interstellar / Event Horizon artwork
+
+These two original background plates were generated for TokenStep 0.2.7 from the user-approved “Natural Horizon Close-up” design direction.
+
+- `InterstellarEventHorizonHero.png`: normal cinematic exposure for the popover, Today dashboard, Token Island, and share cards.
+- `InterstellarEventHorizonQuiet.png`: lower-luminance plate for settings, history, privacy, the update window, and Reduce Motion states.
+
+The production UI adds its own translucent panels, progress rings, accessibility veil, and lightweight accretion-flow animation. No movie artwork, logos, characters, spacecraft, or third-party watermark is bundled.
diff --git a/docs/INTERSTELLAR_THEME_PACK_0.2.7.md b/docs/INTERSTELLAR_THEME_PACK_0.2.7.md
new file mode 100644
index 0000000..4ef42b7
--- /dev/null
+++ b/docs/INTERSTELLAR_THEME_PACK_0.2.7.md
@@ -0,0 +1,56 @@
+# TokenStep 0.2.7 “Event Horizon” theme implementation
+
+## Product identity
+
+Public theme name: **引力边界 / Event Horizon**.
+
+“Gargantua” and *Interstellar* were used only as research and design references. The shipping theme uses an original name, original generated artwork, TokenStep typography, and a custom event-horizon mark.
+
+## Visual system
+
+- Near-field black-hole shadow extends beyond the upper-left canvas.
+- The horizontal accretion disk is wider than 2.4 times the visible shadow diameter.
+- A bright ivory gravitational cusp rises near the center.
+- The upper lensed arc and thinner inverted lower arc remain visible.
+- UI surfaces use graphite glass with stable contrast independent of the image.
+- Theme accents are pearl ivory, cream, and restrained champagne; synchronization success is the only persistent green status signal.
+
+## Production assets
+
+- `TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonHero.png`
+- `TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonQuiet.png`
+
+The hero plate is used for the popover, Today, Token Island, and share surfaces. The quiet plate is used for denser settings, history, privacy, update, and reduced-motion contexts.
+
+## Motion
+
+The image remains the semantic baseline; all data and progress are understandable with motion disabled. A lightweight SwiftUI overlay adds:
+
+- a 36-second accretion-flow phase drift;
+- low-amplitude cusp breathing;
+- a secondary thin-band drift;
+- a 24 fps ceiling.
+
+Motion is disabled for deterministic screenshots, macOS Reduce Motion, and inactive window surfaces.
+
+## Interface coverage
+
+- Horizontal menu-bar popover and compact/expanded data states
+- Today dashboard
+- History
+- Privacy
+- Settings, including the three-pack selector
+- Update window and download progress
+- Token Island collapsed and expanded states
+- Daily and rhythm share cards
+
+## Theme storage and migration
+
+- New stored theme value: `event_horizon`
+- Theme pack: `interstellar` internally, shown as “引力边界” in the product
+- Classic palette and Odyssey chapter remain independently remembered
+- Older settings without the new value decode unchanged
+
+## Copyright boundary
+
+The release bundles no official poster, film frame, logo, actor likeness, spacecraft, robot, quote, or movie typeface. The black-hole structure follows general scientific visualization features; the final plates are original generated assets.
diff --git a/docs/RELEASE_NOTES_0.2.7.md b/docs/RELEASE_NOTES_0.2.7.md
new file mode 100644
index 0000000..b9bbbdd
--- /dev/null
+++ b/docs/RELEASE_NOTES_0.2.7.md
@@ -0,0 +1,35 @@
+# TokenStep 0.2.7
+
+TokenStep 0.2.7 adds the complete “Event Horizon” black-hole skin pack and makes update checks resilient to GitHub API rate limits.
+
+## New: Event Horizon skin pack
+
+- Adds a third theme pack beside Classic and Odyssey.
+- Uses an original close-up event-horizon composition with a monumental black-hole shadow, ivory accretion disk, upper gravitational lensing arc, and dimmer inverted lower arc.
+- Applies the skin to the popover, Today, History, Privacy, Settings, update window, Token Island, and daily/rhythm share cards.
+- Switches the in-app TokenStep mark to an event-horizon/accretion-disk mark while the theme is active.
+- Uses restrained graphite, midnight blue, pearl ivory, cream, and muted champagne; green remains reserved for synchronization success.
+- Adds a 24 fps maximum ambient layer with slow phase drift and cusp breathing.
+- Pauses motion while rendering screenshots, when the window is inactive, and when macOS Reduce Motion is enabled.
+
+## Update reliability
+
+- Falls back from the GitHub Releases API to GitHub’s public latest-release redirect when the API is rate-limited or temporarily unavailable.
+- Constructs the official versioned DMG URL from the resolved stable release tag.
+- Adds a 10-second cooldown to repeated manual checks so rapid clicks cannot consume the anonymous API quota or leave a spinner appearing stuck.
+- Shows a specific rate-limit recovery message if both channels fail.
+- Keeps the existing signed/notarized DMG verification, replacement, and automatic relaunch flow.
+
+## Compatibility and privacy
+
+- Existing Classic and Odyssey choices continue to decode unchanged.
+- The Event Horizon choice is stored as a dedicated theme value and survives settings round-trips.
+- Token, cost, quota, leaderboard, and local collection semantics are unchanged.
+- The bundled artwork is original and contains no movie logo, actor, still, spacecraft, or third-party watermark.
+
+## Validation
+
+- Swift unit suite, including theme migration, motion policy, update fallback, cooldown, and update presentation tests.
+- Deterministic renders for all major Event Horizon interfaces.
+- App bundle resource check for both hero and quiet artwork.
+- Signed DMG, Developer ID verification, Apple notarization, and installer verification are release gates.
diff --git a/docs/images/tokenstep-0.2.7-event-horizon-popover.jpg b/docs/images/tokenstep-0.2.7-event-horizon-popover.jpg
new file mode 100644
index 0000000..f9550fb
Binary files /dev/null and b/docs/images/tokenstep-0.2.7-event-horizon-popover.jpg differ
diff --git a/script/build_swiftui_and_run.sh b/script/build_swiftui_and_run.sh
index 0053e14..8f3143a 100755
--- a/script/build_swiftui_and_run.sh
+++ b/script/build_swiftui_and_run.sh
@@ -22,7 +22,8 @@ EXECUTABLE="$BUILD_DIR/$PRODUCT_NAME"
HELPER_EXECUTABLE="$BUILD_DIR/$HELPER_NAME"
ICON_FILE="$ROOT_DIR/TokenUsageMenuApp/assets/TokenStepIcon.icns"
ODYSSEY_ASSET_DIR="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey"
-VERSION="${TOKENSTEP_VERSION:-0.2.6}"
+INTERSTELLAR_ASSET_DIR="$ROOT_DIR/TokenUsageMenuApp/assets/interstellar"
+VERSION="${TOKENSTEP_VERSION:-0.2.7}"
LAUNCH=true
VERIFY=false
@@ -128,6 +129,9 @@ fi
if [ -d "$ODYSSEY_ASSET_DIR" ]; then
cp "$ODYSSEY_ASSET_DIR"/*.png "$RESOURCES/"
fi
+if [ -d "$INTERSTELLAR_ASSET_DIR" ]; then
+ cp "$INTERSTELLAR_ASSET_DIR"/*.png "$RESOURCES/"
+fi
cat > "$CONTENTS/Info.plist" <
diff --git a/script/package_release.sh b/script/package_release.sh
index 1b84690..ecd3c96 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.6}"
+VERSION="${TOKENSTEP_VERSION:-0.2.7}"
IDENTITY="${CODE_SIGN_IDENTITY:-}"
NOTARIZE=false
usage() {
cat <<'USAGE'
Usage:
- TOKENSTEP_VERSION=0.2.6 CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" ./script/package_release.sh [--notarize]
+ TOKENSTEP_VERSION=0.2.7 CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" ./script/package_release.sh [--notarize]
Notarization credentials, choose one:
TOKENSTEP_NOTARY_PROFILE="notarytool-profile"
diff --git a/script/render_popover_panel.sh b/script/render_popover_panel.sh
index fc24e96..71fac90 100755
--- a/script/render_popover_panel.sh
+++ b/script/render_popover_panel.sh
@@ -8,7 +8,7 @@ OVERLAY_DIR="$BUILD_DIR/vfs-overlay"
OVERLAY_FILE="$OVERLAY_DIR/overlay.yaml"
EMPTY_MODULEMAP="$OVERLAY_DIR/empty.modulemap"
EXECUTABLE="$BUILD_DIR/popover-panel-render"
-OUTPUT_DIR="${1:-$ROOT_DIR/docs/validation/v0.2.4-popover-theme-packs}"
+OUTPUT_DIR="${1:-$ROOT_DIR/docs/validation/v0.2.7-theme-packs}"
mkdir -p "$BUILD_DIR" "$OVERLAY_DIR" "$OUTPUT_DIR"
cat > "$EMPTY_MODULEMAP" <<'EOF'
@@ -23,13 +23,15 @@ while IFS= read -r source; do SOURCES+=("$source"); done < <(find "$SWIFT_DIR/So
swiftc -D TOKENSTEP_TESTING -target arm64-apple-macos14.0 -vfsoverlay "$OVERLAY_FILE" -Xcc -ivfsoverlay -Xcc "$OVERLAY_FILE" -parse-as-library "${SOURCES[@]}" "$SWIFT_DIR/Tests/Fixtures/PopoverPanelRender.swift" -o "$EXECUTABLE"
-for theme in green voyage; do
- test_root="$(mktemp -d "${TMPDIR:-/tmp}/tokenstep-popover-v024-${theme}.XXXXXX")"
+for theme in green voyage event_horizon; do
+ test_root="$(mktemp -d "${TMPDIR:-/tmp}/tokenstep-popover-v027-${theme}.XXXXXX")"
TOKENSTEP_TEST_APP_SUPPORT_ROOT="$test_root/app-support" \
TOKENSTEP_ICON_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/TokenStepIcon.icns" \
TOKENSTEP_ODYSSEY_AEGEAN_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey/OdysseyAegeanPopover.png" \
TOKENSTEP_ODYSSEY_TROJAN_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey/OdysseyTrojanPopover.png" \
TOKENSTEP_ODYSSEY_ASH_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey/OdysseyAshMarblePopover.png" \
+ TOKENSTEP_INTERSTELLAR_HERO_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonHero.png" \
+ TOKENSTEP_INTERSTELLAR_QUIET_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonQuiet.png" \
TOKENSTEP_ODYSSEY_CHAPTER="${TOKENSTEP_ODYSSEY_CHAPTER:-aegean_mist}" \
TOKENSTEP_POPOVER_THEME="$theme" \
TOKENSTEP_POPOVER_RENDER_PATH="$OUTPUT_DIR/${theme}.png" \
@@ -44,6 +46,8 @@ TOKENSTEP_ICON_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/TokenStepIcon.icns" \
TOKENSTEP_ODYSSEY_AEGEAN_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey/OdysseyAegeanPopover.png" \
TOKENSTEP_ODYSSEY_TROJAN_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey/OdysseyTrojanPopover.png" \
TOKENSTEP_ODYSSEY_ASH_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey/OdysseyAshMarblePopover.png" \
+TOKENSTEP_INTERSTELLAR_HERO_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonHero.png" \
+TOKENSTEP_INTERSTELLAR_QUIET_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonQuiet.png" \
TOKENSTEP_ODYSSEY_CHAPTER="${TOKENSTEP_ODYSSEY_CHAPTER:-aegean_mist}" \
TOKENSTEP_POPOVER_THEME="voyage" \
TOKENSTEP_POPOVER_RANK="hidden" \
diff --git a/script/render_voyage_interfaces.sh b/script/render_voyage_interfaces.sh
index d5061d6..2cb9f9b 100755
--- a/script/render_voyage_interfaces.sh
+++ b/script/render_voyage_interfaces.sh
@@ -8,7 +8,7 @@ OVERLAY_DIR="$BUILD_DIR/vfs-overlay"
OVERLAY_FILE="$OVERLAY_DIR/overlay.yaml"
EMPTY_MODULEMAP="$OVERLAY_DIR/empty.modulemap"
EXECUTABLE="$BUILD_DIR/voyage-interface-render"
-OUTPUT_DIR="${1:-$ROOT_DIR/docs/validation/v0.2.4-odyssey-directors-cut}"
+OUTPUT_DIR="${1:-$ROOT_DIR/docs/validation/v0.2.7-interstellar}"
mkdir -p "$BUILD_DIR" "$OVERLAY_DIR" "$OUTPUT_DIR"
cat > "$EMPTY_MODULEMAP" <<'EOF'
@@ -23,7 +23,7 @@ while IFS= read -r source; do SOURCES+=("$source"); done < <(find "$SWIFT_DIR/So
swiftc -D TOKENSTEP_TESTING -target arm64-apple-macos14.0 -vfsoverlay "$OVERLAY_FILE" -Xcc -ivfsoverlay -Xcc "$OVERLAY_FILE" -parse-as-library "${SOURCES[@]}" "$SWIFT_DIR/Tests/Fixtures/VoyageInterfaceRender.swift" -o "$EXECUTABLE"
-test_root="$(mktemp -d "${TMPDIR:-/tmp}/tokenstep-odyssey-v024.XXXXXX")"
+test_root="$(mktemp -d "${TMPDIR:-/tmp}/tokenstep-theme-v027.XXXXXX")"
trap 'rm -rf "$test_root"' EXIT
TOKENSTEP_TEST_APP_SUPPORT_ROOT="$test_root/app-support" \
@@ -31,7 +31,10 @@ TOKENSTEP_ICON_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/TokenStepIcon.icns" \
TOKENSTEP_ODYSSEY_AEGEAN_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey/OdysseyAegeanPopover.png" \
TOKENSTEP_ODYSSEY_TROJAN_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey/OdysseyTrojanPopover.png" \
TOKENSTEP_ODYSSEY_ASH_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey/OdysseyAshMarblePopover.png" \
+TOKENSTEP_INTERSTELLAR_HERO_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonHero.png" \
+TOKENSTEP_INTERSTELLAR_QUIET_ART_PATH="$ROOT_DIR/TokenUsageMenuApp/assets/interstellar/InterstellarEventHorizonQuiet.png" \
TOKENSTEP_ODYSSEY_CHAPTER="${TOKENSTEP_ODYSSEY_CHAPTER:-directors_cut}" \
+TOKENSTEP_THEME_PACK="${TOKENSTEP_THEME_PACK:-interstellar}" \
TOKENSTEP_VOYAGE_RENDER_DIR="$OUTPUT_DIR" \
"$EXECUTABLE"