mirror of
https://github.com/VGEAREN/Stockbar.git
synced 2026-10-06 14:43:34 +08:00
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:
co-authored by
Claude Sonnet 4.6
parent
ba0ce58662
commit
41665a6308
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ struct DropdownView: View {
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
.frame(maxHeight: 520)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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]))
|
||||
|
||||
Reference in New Issue
Block a user