Add US session zones, check-for-updates, UI polish

- StockChartView: mark 盘前/盘中/盘后 with colored background zones and text labels for US stocks
- UpdateChecker: check latest release via GitHub API, prompt user to download if newer version found; semantic version comparison to avoid false positives
- SettingsView: add 关于 section with version and 检查更新 button; hide scrollbar (.never)
- DropdownView: hide scrollbar (.never)
- ProfitSummaryView: replace opaque white background with subtle transparent tint
- project.pbxproj: normalize MARKETING_VERSION to 1.0.0 (three-part semver)
- README/README_CN: add main screenshot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
vgearen
2026-03-18 11:43:27 +08:00
co-authored by Claude Sonnet 4.6
parent ba0ce58662
commit 41665a6308
9 changed files with 157 additions and 7 deletions
+80
View File
@@ -0,0 +1,80 @@
import Foundation
import AppKit
import Combine
private struct GitHubRelease: Decodable {
let tagName: String
let htmlUrl: String
enum CodingKeys: String, CodingKey {
case tagName = "tag_name"
case htmlUrl = "html_url"
}
}
@MainActor
final class UpdateChecker: ObservableObject {
static let shared = UpdateChecker()
private init() {}
@Published var isChecking = false
func checkForUpdates() {
guard !isChecking else { return }
isChecking = true
Task { await run() }
}
private func run() async {
defer { isChecking = false }
guard let url = URL(string: "https://api.github.com/repos/VGEAREN/Stockbar/releases/latest") else { return }
var req = URLRequest(url: url)
req.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
guard let (data, _) = try? await URLSession.shared.data(for: req),
let release = try? JSONDecoder().decode(GitHubRelease.self, from: data) else {
alert(title: "检查失败", body: "无法获取版本信息,请检查网络连接。")
return
}
let latestTag = release.tagName
let latest = latestTag.hasPrefix("v") ? String(latestTag.dropFirst()) : latestTag
let current = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0"
if isNewer(latest, than: current) {
let a = NSAlert()
a.messageText = "发现新版本 \(latest)"
a.informativeText = "当前版本 \(current)。下载 DMG 后将旧版本替换即可完成更新。"
a.addButton(withTitle: "前往下载")
a.addButton(withTitle: "暂不更新")
if a.runModal() == .alertFirstButtonReturn,
let releaseURL = URL(string: release.htmlUrl) {
NSWorkspace.shared.open(releaseURL)
}
} else {
alert(title: "已是最新版本", body: "当前版本 \(current) 已是最新。")
}
}
/// 逐段比较语义版本,"1.0" 与 "1.0.0" 视为相等,不误报
private func isNewer(_ remote: String, than local: String) -> Bool {
let r = remote.split(separator: ".").compactMap { Int($0) }
let l = local.split(separator: ".").compactMap { Int($0) }
let len = max(r.count, l.count)
for i in 0..<len {
let rv = i < r.count ? r[i] : 0
let lv = i < l.count ? l[i] : 0
if rv != lv { return rv > lv }
}
return false
}
private func alert(title: String, body: String) {
let a = NSAlert()
a.messageText = title
a.informativeText = body
a.addButton(withTitle: "好")
a.runModal()
}
}
+1
View File
@@ -45,6 +45,7 @@ struct DropdownView: View {
}
.padding(.horizontal, 6)
}
.scrollIndicators(.never)
.frame(maxHeight: 520)
}
+1 -1
View File
@@ -35,7 +35,7 @@ struct ProfitSummaryView: View {
}
.padding(.horizontal, 8)
.padding(.vertical, 5)
.background(Color(nsColor: .controlBackgroundColor))
.background(Color.primary.opacity(0.04))
.cornerRadius(6)
}
}
+30
View File
@@ -6,6 +6,8 @@ struct SettingsView: View {
@EnvironmentObject var appState: AppState
@Binding var showSettings: Bool
@ObservedObject private var updater = UpdateChecker.shared
@State private var launchAtLogin = (SMAppService.mainApp.status == .enabled)
@State private var searchText = ""
@State private var searchResults = [SearchResult]()
@@ -21,6 +23,7 @@ struct SettingsView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
if let s = editingStock {
inlineEditView(s)
} else {
@@ -93,6 +96,32 @@ struct SettingsView: View {
}
}
// 关于 & 更新
section("关于") {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Stockbar")
.font(.system(size: 12, weight: .medium))
Text("版本 \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "-")")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Button {
UpdateChecker.shared.checkForUpdates()
} label: {
if UpdateChecker.shared.isChecking {
ProgressView().scaleEffect(0.6).frame(width: 60)
} else {
Text("检查更新")
}
}
.buttonStyle(.borderedProminent)
.controlSize(.small)
.disabled(UpdateChecker.shared.isChecking)
}
}
// 添加股票
section("添加股票") {
HStack {
@@ -136,6 +165,7 @@ struct SettingsView: View {
}
.padding(8)
}
.scrollIndicators(.never)
.frame(width: 284, height: 480)
}
+35
View File
@@ -106,6 +106,13 @@ struct StockChartView: View {
}
}
// 美股时段定义:x 轴 index(基准 04:00 ET,每分钟 +1)
private let usSessions: [(name: String, start: Int, end: Int, color: Color)] = [
("盘前", 0, 329, Color.indigo),
("盘中", 330, 719, Color.teal),
("盘后", 720, 959, Color.orange),
]
private var lineChart: some View {
let prices = points.map(\.price)
let minP = prices.min() ?? preClose
@@ -120,8 +127,36 @@ struct StockChartView: View {
: Color(appState.config.downColorName)
let labelPositions = xAxisLabels.map(\.0)
let isUS = stock.market == .usStock
let labelY = yMin + (yMax - yMin) * 0.88
return Chart {
// 美股时段背景色块
if isUS {
ForEach(usSessions, id: \.name) { s in
RectangleMark(
xStart: .value("xs", s.start),
xEnd: .value("xe", s.end),
yStart: .value("ys", yMin),
yEnd: .value("ye", yMax)
)
.foregroundStyle(s.color.opacity(0.07))
}
// 美股时段文字标注
ForEach(usSessions, id: \.name) { s in
PointMark(
x: .value("t", (s.start + s.end) / 2),
y: .value("price", labelY)
)
.symbolSize(0)
.annotation(position: .overlay) {
Text(s.name)
.font(.system(size: 9, weight: .medium))
.foregroundColor(s.color.opacity(0.65))
}
}
}
// 昨收参考线
RuleMark(y: .value("昨收", preClose))
.lineStyle(StrokeStyle(lineWidth: 0.8, dash: [4, 4]))