From a543996f526415750775da3278d4325915b6ccbf Mon Sep 17 00:00:00 2001 From: zhenan <1254455745@qq.com> Date: Sun, 31 May 2026 13:29:19 +0800 Subject: [PATCH 1/6] Improve menu ticker and portfolio UI --- .../Repositories/SettingsRepository.swift | 11 + PanBar/Domain/Services/PortfolioService.swift | 4 +- PanBar/Infrastructure/HotkeyBinding.swift | 29 +- .../Infrastructure/NotificationService.swift | 14 +- PanBar/Infrastructure/QuoteRefresher.swift | 31 +- PanBar/Infrastructure/TickerPreferences.swift | 124 +- PanBar/Infrastructure/Updater.swift | 4 +- PanBar/MenuBar/CarouselTickerView.swift | 37 +- PanBar/MenuBar/CompactTickerView.swift | 43 +- PanBar/MenuBar/MenuBarTickerView.swift | 6 + PanBar/MenuBar/MinimalTickerView.swift | 32 +- PanBar/MenuBar/StatusItemController.swift | 84 +- PanBar/MenuBar/TickerRenderer.swift | 16 +- PanBar/MenuBar/TickerView.swift | 32 +- PanBar/Popover/PopoverController.swift | 26 +- PanBar/Popover/Views/HoldingsTab.swift | 79 +- PanBar/Popover/Views/SummaryCards.swift | 14 +- PanBar/Resources/Localizable.xcstrings | 5728 +++++++++++++---- PanBar/Settings/Panes/PortfolioPane.swift | 131 +- PanBar/Settings/Panes/TickerPane.swift | 154 +- project.yml | 1 + 21 files changed, 5235 insertions(+), 1365 deletions(-) diff --git a/PanBar/Data/Persistence/Repositories/SettingsRepository.swift b/PanBar/Data/Persistence/Repositories/SettingsRepository.swift index c75ffed..5af4f88 100644 --- a/PanBar/Data/Persistence/Repositories/SettingsRepository.swift +++ b/PanBar/Data/Persistence/Repositories/SettingsRepository.swift @@ -57,6 +57,17 @@ struct SettingsRepository { static let tickerShowTotalAssets = "ticker_show_total_assets" static let tickerShowTodayPnL = "ticker_show_today_pnl" static let tickerShowAllTimePnL = "ticker_show_alltime_pnl" + static let tickerShowAppIcon = "ticker_show_app_icon" + static let tickerShowQuoteCode = "ticker_show_quote_code" + static let tickerShowQuoteName = "ticker_show_quote_name" + static let tickerMenuBarWidth = "ticker_menu_bar_width" + static let tickerScrollMenuBarWidth = "ticker_scroll_menu_bar_width" + static let tickerCarouselMenuBarWidth = "ticker_carousel_menu_bar_width" + static let tickerCompactMenuBarWidth = "ticker_compact_menu_bar_width" + static let tickerScrollAutoWidth = "ticker_scroll_auto_width" + static let tickerCarouselAutoWidth = "ticker_carousel_auto_width" + static let tickerCompactAutoWidth = "ticker_compact_auto_width" + static let tickerShowDirectionArrow = "ticker_show_direction_arrow" static let hideOnScreenShare = "hide_on_screen_share" static let privacyManualHide = "privacy_manual_hide" static let tickerIndexIDs = "ticker_index_ids" diff --git a/PanBar/Domain/Services/PortfolioService.swift b/PanBar/Domain/Services/PortfolioService.swift index 4d3773e..ee137a1 100644 --- a/PanBar/Domain/Services/PortfolioService.swift +++ b/PanBar/Domain/Services/PortfolioService.swift @@ -36,7 +36,9 @@ actor PortfolioService { if allSymbols.isEmpty { quotes = [:] } else { - quotes = (try? await provider.fetch(Array(allSymbols))) ?? [:] + let fetched = try await provider.fetch(Array(allSymbols)) + guard !fetched.isEmpty else { throw ProviderError.empty } + quotes = fetched } let converter = await fx.currentConverter() diff --git a/PanBar/Infrastructure/HotkeyBinding.swift b/PanBar/Infrastructure/HotkeyBinding.swift index f697166..a93a630 100644 --- a/PanBar/Infrastructure/HotkeyBinding.swift +++ b/PanBar/Infrastructure/HotkeyBinding.swift @@ -84,24 +84,25 @@ struct HotkeyBinding: Codable, Equatable, Sendable { guard let source = TISCopyCurrentASCIICapableKeyboardLayoutInputSource()?.takeRetainedValue() else { return nil } guard let layoutData = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else { return nil } let dataRef = unsafeBitCast(layoutData, to: CFData.self) - let layoutPtr = CFDataGetBytePtr(dataRef)! - let keyLayout = unsafeBitCast(layoutPtr, to: UnsafePointer.self) + guard let layoutPtr = CFDataGetBytePtr(dataRef) else { return nil } var deadKeyState: UInt32 = 0 var actualLength = 0 var chars: [UniChar] = Array(repeating: 0, count: 4) - let status = UCKeyTranslate( - keyLayout, - keyCode, - UInt16(kUCKeyActionDisplay), - 0, - UInt32(LMGetKbdType()), - UInt32(kUCKeyTranslateNoDeadKeysBit), - &deadKeyState, - chars.count, - &actualLength, - &chars - ) + let status = layoutPtr.withMemoryRebound(to: UCKeyboardLayout.self, capacity: 1) { keyLayout in + UCKeyTranslate( + keyLayout, + keyCode, + UInt16(kUCKeyActionDisplay), + 0, + UInt32(LMGetKbdType()), + UInt32(kUCKeyTranslateNoDeadKeysBit), + &deadKeyState, + chars.count, + &actualLength, + &chars + ) + } guard status == noErr, actualLength > 0 else { return nil } return String(utf16CodeUnits: chars, count: actualLength) } diff --git a/PanBar/Infrastructure/NotificationService.swift b/PanBar/Infrastructure/NotificationService.swift index 7cd9716..dca50c6 100644 --- a/PanBar/Infrastructure/NotificationService.swift +++ b/PanBar/Infrastructure/NotificationService.swift @@ -40,7 +40,7 @@ final class NotificationService: NSObject, ObservableObject { } else { Log.app.info("notification auth granted=\(granted, privacy: .public)") } - DispatchQueue.main.async { + Task { @MainActor in self?.refreshAuthorizationStatus() } } @@ -48,7 +48,7 @@ final class NotificationService: NSObject, ObservableObject { func refreshAuthorizationStatus() { center.getNotificationSettings { [weak self] settings in - DispatchQueue.main.async { + Task { @MainActor in self?.authorizationStatus = settings.authorizationStatus } } @@ -72,16 +72,20 @@ final class NotificationService: NSObject, ObservableObject { center.add(request) { [weak self] error in if let error = error { Log.app.error("notification add failed: \(String(describing: error), privacy: .public)") - self?.diag("add failed: \(error)") + Task { @MainActor in + self?.diag("add failed: \(error)") + } } else { - self?.diag("add ok id=\(identifier)") + Task { @MainActor in + self?.diag("add ok id=\(identifier)") + } } } // 同步检查权限,如果是 denied 就走弹窗 fallback center.getNotificationSettings { [weak self] settings in if settings.authorizationStatus == .denied { - DispatchQueue.main.async { + Task { @MainActor in self?.showFallbackAlert(title: title, body: body) } } diff --git a/PanBar/Infrastructure/QuoteRefresher.swift b/PanBar/Infrastructure/QuoteRefresher.swift index d3bcc3a..483e55b 100644 --- a/PanBar/Infrastructure/QuoteRefresher.swift +++ b/PanBar/Infrastructure/QuoteRefresher.swift @@ -224,32 +224,33 @@ final class QuoteRefresher: ObservableObject { } private func tick() async { - await MainActor.run { self.isRefreshing = true } + guard !isRefreshing else { return } + isRefreshing = true + defer { isRefreshing = false } + do { let snap = try await service.computeSnapshot() - // 网络空响应(provider.fetch 失败 -> quotes 空)时,不要拿一个全 nil quote 的 - // snapshot 去覆盖磁盘 seed 的好数据;原样保留 cached snapshot,等下次 tick。 let hasFreshData = !snap.allQuotes.isEmpty - await MainActor.run { - if hasFreshData { - self.snapshot = snap - self.snapshotIsFromCache = false - self.quotes.merge(snap.allQuotes) { _, new in new } - self.lastUpdated = Date() - self.lastError = nil - self.alertEngine?.evaluate(quotes: self.quotes) + let isEmptyPortfolio = snap.positions.isEmpty && snap.allQuotes.isEmpty + if hasFreshData || isEmptyPortfolio { + snapshot = snap + snapshotIsFromCache = false + if isEmptyPortfolio { + quotes = [:] + } else { + quotes.merge(snap.allQuotes) { _, new in new } } + lastUpdated = Date() + lastError = nil + alertEngine?.evaluate(quotes: quotes) } // 拉到了就异步写盘,下次冷启动可以秒读 if hasFreshData { quoteCacheRepo?.upsertMany(snap.allQuotes) } } catch { - await MainActor.run { - self.lastError = String(describing: error) - } + lastError = String(describing: error) Log.quote.error("refresh failed: \(String(describing: error), privacy: .public)") } - await MainActor.run { self.isRefreshing = false } } } diff --git a/PanBar/Infrastructure/TickerPreferences.swift b/PanBar/Infrastructure/TickerPreferences.swift index a1e2e7b..0173046 100644 --- a/PanBar/Infrastructure/TickerPreferences.swift +++ b/PanBar/Infrastructure/TickerPreferences.swift @@ -32,6 +32,67 @@ final class TickerPreferences: ObservableObject { @Published var showAllTimePnL: Bool { didSet { try? repo.set(SettingsRepository.Keys.tickerShowAllTimePnL, showAllTimePnL ? "1" : "0") } } + @Published var showAppIcon: Bool { + didSet { try? repo.set(SettingsRepository.Keys.tickerShowAppIcon, showAppIcon ? "1" : "0") } + } + @Published var showQuoteCode: Bool { + didSet { try? repo.set(SettingsRepository.Keys.tickerShowQuoteCode, showQuoteCode ? "1" : "0") } + } + @Published var showQuoteName: Bool { + didSet { try? repo.set(SettingsRepository.Keys.tickerShowQuoteName, showQuoteName ? "1" : "0") } + } + @Published var menuBarWidth: Int { + didSet { + let clamped = min(520, max(80, menuBarWidth)) + if clamped != menuBarWidth { + menuBarWidth = clamped + } else { + try? repo.set(SettingsRepository.Keys.tickerMenuBarWidth, "\(menuBarWidth)") + } + } + } + @Published var scrollMenuBarWidth: Int { + didSet { + let clamped = min(720, max(160, scrollMenuBarWidth)) + if clamped != scrollMenuBarWidth { + scrollMenuBarWidth = clamped + } else { + try? repo.set(SettingsRepository.Keys.tickerScrollMenuBarWidth, "\(scrollMenuBarWidth)") + } + } + } + @Published var carouselMenuBarWidth: Int { + didSet { + let clamped = min(360, max(100, carouselMenuBarWidth)) + if clamped != carouselMenuBarWidth { + carouselMenuBarWidth = clamped + } else { + try? repo.set(SettingsRepository.Keys.tickerCarouselMenuBarWidth, "\(carouselMenuBarWidth)") + } + } + } + @Published var compactMenuBarWidth: Int { + didSet { + let clamped = min(360, max(60, compactMenuBarWidth)) + if clamped != compactMenuBarWidth { + compactMenuBarWidth = clamped + } else { + try? repo.set(SettingsRepository.Keys.tickerCompactMenuBarWidth, "\(compactMenuBarWidth)") + } + } + } + @Published var scrollAutoWidth: Bool { + didSet { try? repo.set(SettingsRepository.Keys.tickerScrollAutoWidth, scrollAutoWidth ? "1" : "0") } + } + @Published var carouselAutoWidth: Bool { + didSet { try? repo.set(SettingsRepository.Keys.tickerCarouselAutoWidth, carouselAutoWidth ? "1" : "0") } + } + @Published var compactAutoWidth: Bool { + didSet { try? repo.set(SettingsRepository.Keys.tickerCompactAutoWidth, compactAutoWidth ? "1" : "0") } + } + @Published var showDirectionArrow: Bool { + didSet { try? repo.set(SettingsRepository.Keys.tickerShowDirectionArrow, showDirectionArrow ? "1" : "0") } + } /// 哪些大盘指数显示在滚动条中(存 IndexDescriptor.id 集合)。 @Published var tickerIndexIDs: Set { didSet { @@ -63,10 +124,26 @@ final class TickerPreferences: ObservableObject { self.pauseOnHover = repo.string(SettingsRepository.Keys.pauseOnHover) != "0" self.pauseWhenClosed = repo.string(SettingsRepository.Keys.pauseWhenClosed) != "0" self.maxItems = Int(repo.string(SettingsRepository.Keys.maxTickerItems) ?? "10") ?? 10 - // Summary 三项默认不显示(避免菜单栏一启动就长),用户主动开启 - self.showTotalAssets = repo.string(SettingsRepository.Keys.tickerShowTotalAssets) == "1" - self.showTodayPnL = repo.string(SettingsRepository.Keys.tickerShowTodayPnL) == "1" - self.showAllTimePnL = repo.string(SettingsRepository.Keys.tickerShowAllTimePnL) == "1" + let storedDisplayMode = TickerDisplayMode(rawValue: repo.string(SettingsRepository.Keys.tickerDisplayMode) ?? "") ?? .scroll + let storedMinimalMetric = MinimalMetric(rawValue: repo.string(SettingsRepository.Keys.tickerMinimalMetric) ?? "") ?? .todayPnL + let migratesMinimal = storedDisplayMode == .minimal + // Summary 三项默认不显示(避免菜单栏一启动就长),用户主动开启。 + // 旧版「极简」迁移为「固定」:只保留原来极简选择的那个指标。 + if migratesMinimal { + let migratedShowTotalAssets = storedMinimalMetric == .totalAssets + let migratedShowTodayPnL = storedMinimalMetric == .todayPnL + let migratedShowAllTimePnL = storedMinimalMetric == .allTimePnL + self.showTotalAssets = migratedShowTotalAssets + self.showTodayPnL = migratedShowTodayPnL + self.showAllTimePnL = migratedShowAllTimePnL + try? repo.set(SettingsRepository.Keys.tickerShowTotalAssets, migratedShowTotalAssets ? "1" : "0") + try? repo.set(SettingsRepository.Keys.tickerShowTodayPnL, migratedShowTodayPnL ? "1" : "0") + try? repo.set(SettingsRepository.Keys.tickerShowAllTimePnL, migratedShowAllTimePnL ? "1" : "0") + } else { + self.showTotalAssets = repo.string(SettingsRepository.Keys.tickerShowTotalAssets) == "1" + self.showTodayPnL = repo.string(SettingsRepository.Keys.tickerShowTodayPnL) == "1" + self.showAllTimePnL = repo.string(SettingsRepository.Keys.tickerShowAllTimePnL) == "1" + } if let json = repo.string(SettingsRepository.Keys.tickerIndexIDs), let data = json.data(using: .utf8), let arr = try? JSONDecoder().decode([String].self, from: data) { @@ -74,23 +151,58 @@ final class TickerPreferences: ObservableObject { } else { self.tickerIndexIDs = [] } - self.displayMode = TickerDisplayMode(rawValue: repo.string(SettingsRepository.Keys.tickerDisplayMode) ?? "") ?? .scroll - self.minimalMetric = MinimalMetric(rawValue: repo.string(SettingsRepository.Keys.tickerMinimalMetric) ?? "") ?? .todayPnL + self.displayMode = storedDisplayMode == .scrollNoCode ? .scroll : (migratesMinimal ? .compact : storedDisplayMode) + if migratesMinimal { + try? repo.set(SettingsRepository.Keys.tickerDisplayMode, TickerDisplayMode.compact.rawValue) + } + self.minimalMetric = storedMinimalMetric self.carouselDwell = Int(repo.string(SettingsRepository.Keys.tickerCarouselDwell) ?? "") ?? 4 + self.showAppIcon = repo.string(SettingsRepository.Keys.tickerShowAppIcon) != "0" + if storedDisplayMode == .scrollNoCode { + self.showQuoteCode = false + } else { + self.showQuoteCode = repo.string(SettingsRepository.Keys.tickerShowQuoteCode) != "0" + } + self.showQuoteName = repo.string(SettingsRepository.Keys.tickerShowQuoteName) != "0" + let legacyWidth = Int(repo.string(SettingsRepository.Keys.tickerMenuBarWidth) ?? "") ?? 280 + self.menuBarWidth = legacyWidth + self.scrollMenuBarWidth = Int(repo.string(SettingsRepository.Keys.tickerScrollMenuBarWidth) ?? "") ?? max(160, legacyWidth) + self.carouselMenuBarWidth = Int(repo.string(SettingsRepository.Keys.tickerCarouselMenuBarWidth) ?? "") ?? min(360, max(160, legacyWidth)) + self.compactMenuBarWidth = Int(repo.string(SettingsRepository.Keys.tickerCompactMenuBarWidth) ?? "") ?? 160 + self.scrollAutoWidth = repo.string(SettingsRepository.Keys.tickerScrollAutoWidth) == "1" + self.carouselAutoWidth = repo.string(SettingsRepository.Keys.tickerCarouselAutoWidth) == "1" + self.compactAutoWidth = repo.string(SettingsRepository.Keys.tickerCompactAutoWidth) != "0" + self.showDirectionArrow = migratesMinimal || repo.string(SettingsRepository.Keys.tickerShowDirectionArrow) == "1" + if migratesMinimal { + try? repo.set(SettingsRepository.Keys.tickerShowDirectionArrow, "1") + } } + } /// 菜单栏 ticker 的展现形式。 enum TickerDisplayMode: String, CaseIterable, Identifiable, Codable { case scroll // 经典左右滚动(默认) + case scrollNoCode // 旧版本兼容:现在用 showQuoteCode 控制 case carousel // 一条一条上下淡入轮播 case compact // 三个简写卡片(今日 / 总盈亏 / 总市值) case minimal // 只显示一个用户选定的数字 + static let allCases: [TickerDisplayMode] = [.scroll, .carousel, .compact] + var id: String { rawValue } + + var usesScrollingView: Bool { + switch self { + case .scroll, .scrollNoCode: return true + case .carousel, .compact, .minimal: return false + } + } + var displayName: String { switch self { case .scroll: return L("displayMode.scroll", comment: "") + case .scrollNoCode: return L("displayMode.scroll", comment: "") case .carousel: return L("displayMode.carousel", comment: "") case .compact: return L("displayMode.compact", comment: "") case .minimal: return L("displayMode.minimal", comment: "") diff --git a/PanBar/Infrastructure/Updater.swift b/PanBar/Infrastructure/Updater.swift index 888c36d..34fab46 100644 --- a/PanBar/Infrastructure/Updater.swift +++ b/PanBar/Infrastructure/Updater.swift @@ -50,11 +50,11 @@ final class Updater: NSObject, ObservableObject { } do { let release = try await self.fetchLatestRelease() - await self.handleResult(release: release, silent: silent) + self.handleResult(release: release, silent: silent) } catch { Log.app.warning("update check failed: \(String(describing: error), privacy: .public)") if !silent { - await self.showError(error) + self.showError(error) } } } diff --git a/PanBar/MenuBar/CarouselTickerView.swift b/PanBar/MenuBar/CarouselTickerView.swift index 1ef1e6b..e9fd62e 100644 --- a/PanBar/MenuBar/CarouselTickerView.swift +++ b/PanBar/MenuBar/CarouselTickerView.swift @@ -21,11 +21,28 @@ final class CarouselTickerView: NSView { var onContentChanged: (() -> Void)? let iconWidth: CGFloat = 18 + var showsIcon: Bool = true + var preferredTotalWidth: CGFloat? var visibleTextWidth: CGFloat = 200 var privacyHidden: Bool = false var totalWidth: CGFloat { - iconWidth + 6 + visibleTextWidth + 4 + if items.isEmpty { + return leadingTextX + 4 + } + if let preferredTotalWidth { + return max(40, preferredTotalWidth) + } + return leadingTextX + currentTextWidth + 4 + } + + private var leadingTextX: CGFloat { + showsIcon ? iconWidth + 6 : 2 + } + + private var currentTextWidth: CGFloat { + guard items.indices.contains(currentIndex) else { return 0 } + return min(360, max(20, ceil(items[currentIndex].size().width) + 8)) } override var isFlipped: Bool { false } @@ -56,7 +73,11 @@ final class CarouselTickerView: NSView { self.items = items if currentIndex >= items.count { currentIndex = 0 } let maxW = items.map { $0.size().width }.max() ?? 0 - visibleTextWidth = max(80, maxW + 8) + if maxW > 0 { + visibleTextWidth = min(360, max(100, maxW + 8)) + } else { + visibleTextWidth = 0 + } needsDisplay = true onContentChanged?() } @@ -83,6 +104,10 @@ final class CarouselTickerView: NSView { displayLink = nil } + func invalidateAnimation() { + stopAnimation() + } + private func step(now: CFTimeInterval) { if items.count <= 1 { return @@ -115,12 +140,14 @@ final class CarouselTickerView: NSView { } override func draw(_ dirtyRect: NSRect) { - drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth)) + if showsIcon { + drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth)) + } let textRect = NSRect( - x: iconWidth + 6, + x: leadingTextX, y: 0, - width: visibleTextWidth, + width: preferredTotalWidth == nil ? currentTextWidth : max(20, totalWidth - leadingTextX - 4), height: bounds.height ) diff --git a/PanBar/MenuBar/CompactTickerView.swift b/PanBar/MenuBar/CompactTickerView.swift index 1b574cd..1801dc6 100644 --- a/PanBar/MenuBar/CompactTickerView.swift +++ b/PanBar/MenuBar/CompactTickerView.swift @@ -13,6 +13,9 @@ final class CompactTickerView: NSView { private var slots: Slots = Slots(todayPnL: nil, allTimePnL: nil, totalAssets: nil, baseCurrency: .cny) var scheme: TickerColorScheme = .east var privacyHidden: Bool = false + var showsIcon: Bool = true + var showsDirectionArrow: Bool = false + var preferredTotalWidth: CGFloat? /// hover 状态(放着满足协议,固定模式实际用不到) var hovered: Bool = false var onContentChanged: (() -> Void)? @@ -35,7 +38,16 @@ final class CompactTickerView: NSView { } var totalWidth: CGFloat { - iconWidth + 6 + max(60, contentWidth) + 4 + let width = contentWidth + guard width > 0 else { return leadingTextX + 4 } + if let preferredTotalWidth { + return max(40, preferredTotalWidth) + } + return leadingTextX + width + 4 + } + + private var leadingTextX: CGFloat { + showsIcon ? iconWidth + 6 : 2 } override var isFlipped: Bool { false } @@ -64,18 +76,30 @@ final class CompactTickerView: NSView { } override func draw(_ dirtyRect: NSRect) { - drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth)) + if showsIcon { + drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth)) + } + + let textRect = NSRect( + x: leadingTextX, + y: 0, + width: max(20, totalWidth - leadingTextX - 4), + height: bounds.height + ) if privacyHidden { let dots = NSAttributedString(string: "•••", attributes: [ .font: valueFont, .foregroundColor: NSColor.secondaryLabelColor ]) - dots.draw(at: NSPoint(x: iconWidth + 8, y: bounds.midY - dots.size().height / 2)) + dots.draw(at: NSPoint(x: textRect.minX + 4, y: bounds.midY - dots.size().height / 2)) return } - var x: CGFloat = iconWidth + 6 + let ctx = NSGraphicsContext.current?.cgContext + ctx?.saveGState() + NSBezierPath(rect: textRect).addClip() + var x: CGFloat = textRect.minX for (i, piece) in renderPieces().enumerated() { if i > 0 { x += slotSpacing } let size = piece.size() @@ -83,6 +107,7 @@ final class CompactTickerView: NSView { piece.draw(at: NSPoint(x: x, y: y)) x += size.width } + ctx?.restoreGState() } /// 当前要展示的三个简写片段(组合 label + 数字 + 颜色,返回 AttributedString)。 @@ -96,7 +121,7 @@ final class CompactTickerView: NSView { if let pnl = slots.allTimePnL { out.append(piece(label: L("compact.label.allTime", comment: ""), value: pnl, direction: directionOf(pnl))) } - if let v = slots.totalAssets, v > 0 { + if let v = slots.totalAssets { out.append(piece(label: L("compact.label.total", comment: ""), value: v, direction: .neutral)) } return out @@ -125,8 +150,14 @@ final class CompactTickerView: NSView { if direction == .neutral { text = NumberAbbreviation.formatCurrency(value, currency: slots.baseCurrency) } else { + let arrow: String + if showsDirectionArrow { + arrow = direction == .up ? "↑ " : "↓ " + } else { + arrow = "" + } let sign = value < 0 ? "-" : "+" - text = sign + slots.baseCurrency.symbol + NumberAbbreviation.format(value, currency: slots.baseCurrency) + text = arrow + sign + slots.baseCurrency.symbol + NumberAbbreviation.format(value, currency: slots.baseCurrency) } s.append(NSAttributedString(string: text, attributes: valueAttr)) return s diff --git a/PanBar/MenuBar/MenuBarTickerView.swift b/PanBar/MenuBar/MenuBarTickerView.swift index c51f069..158d5ef 100644 --- a/PanBar/MenuBar/MenuBarTickerView.swift +++ b/PanBar/MenuBar/MenuBarTickerView.swift @@ -9,12 +9,16 @@ import AppKit protocol MenuBarTickerView: NSView { var totalWidth: CGFloat { get } var privacyHidden: Bool { get set } + var showsIcon: Bool { get set } + var preferredTotalWidth: CGFloat? { get set } /// 鼠标 hover 状态;由 controller 监听 button 的 tracking area 同步过来 var hovered: Bool { get set } /// 内容变化通知(动画帧 / 数据更新),controller 重新捕图。 var onContentChanged: (() -> Void)? { get set } /// 暂停动画(全市场休市 / 用户开关) func setPaused(_ paused: Bool) + /// view 被替换前停止内部动画源,避免旧 display link 的异步回调撞到新模式。 + func invalidateAnimation() /// 把当前状态画进 NSImage。size 跟 totalWidth × 22 一致。 func renderImage() -> NSImage } @@ -51,9 +55,11 @@ extension CarouselTickerView: MenuBarTickerView { extension CompactTickerView: MenuBarTickerView { func renderImage() -> NSImage { defaultRenderImage() } func setPaused(_ paused: Bool) {} // 无动画 + func invalidateAnimation() {} } extension MinimalTickerView: MenuBarTickerView { func renderImage() -> NSImage { defaultRenderImage() } func setPaused(_ paused: Bool) {} // 无动画 + func invalidateAnimation() {} } diff --git a/PanBar/MenuBar/MinimalTickerView.swift b/PanBar/MenuBar/MinimalTickerView.swift index 6cf0b95..8cfb285 100644 --- a/PanBar/MenuBar/MinimalTickerView.swift +++ b/PanBar/MenuBar/MinimalTickerView.swift @@ -13,6 +13,8 @@ final class MinimalTickerView: NSView { private var content: Content? var scheme: TickerColorScheme = .east var privacyHidden: Bool = false + var showsIcon: Bool = true + var preferredTotalWidth: CGFloat? var hovered: Bool = false var onContentChanged: (() -> Void)? @@ -20,8 +22,15 @@ final class MinimalTickerView: NSView { private let valueFont = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .semibold) var totalWidth: CGFloat { - guard let _ = content else { return iconWidth + 40 } - return iconWidth + 6 + max(56, renderedString().size().width) + 6 + if let preferredTotalWidth { + return max(40, preferredTotalWidth) + } + guard let _ = content else { return leadingTextX + 40 } + return leadingTextX + max(56, renderedString().size().width) + 6 + } + + private var leadingTextX: CGFloat { + showsIcon ? iconWidth + 6 : 2 } override var isFlipped: Bool { false } @@ -50,21 +59,34 @@ final class MinimalTickerView: NSView { } override func draw(_ dirtyRect: NSRect) { - drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth)) + if showsIcon { + drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth)) + } + + let textRect = NSRect( + x: leadingTextX, + y: 0, + width: max(20, totalWidth - leadingTextX - 4), + height: bounds.height + ) if privacyHidden { let dots = NSAttributedString(string: "•••", attributes: [ .font: valueFont, .foregroundColor: NSColor.secondaryLabelColor ]) - dots.draw(at: NSPoint(x: iconWidth + 8, y: bounds.midY - dots.size().height / 2)) + dots.draw(at: NSPoint(x: textRect.minX + 4, y: bounds.midY - dots.size().height / 2)) return } let str = renderedString() let size = str.size() - let p = NSPoint(x: iconWidth + 6, y: (bounds.height - size.height) / 2) + let p = NSPoint(x: textRect.minX, y: (bounds.height - size.height) / 2) + let ctx = NSGraphicsContext.current?.cgContext + ctx?.saveGState() + NSBezierPath(rect: textRect).addClip() str.draw(at: p) + ctx?.restoreGState() } private func renderedString() -> NSAttributedString { diff --git a/PanBar/MenuBar/StatusItemController.swift b/PanBar/MenuBar/StatusItemController.swift index 6b8de07..8733a61 100644 --- a/PanBar/MenuBar/StatusItemController.swift +++ b/PanBar/MenuBar/StatusItemController.swift @@ -19,6 +19,7 @@ final class StatusItemController { private var screenSharingMonitor: ScreenSharingMonitor? private var privacyHidden: Bool = false private var currentMode: TickerDisplayMode = .scroll + private var lockedPopoverLength: CGFloat? init( refresher: QuoteRefresher, @@ -54,16 +55,34 @@ final class StatusItemController { } configure() + popoverController.onClose = { [weak self] in + self?.unlockPopoverLength() + } applyPrefs() bind() } private func applyPrefs() { - renderer = TickerRenderer(scheme: prefs.colorScheme) + renderer = TickerRenderer( + scheme: prefs.colorScheme, + showsQuoteCode: prefs.showQuoteCode, + showsQuoteName: prefs.showQuoteName + ) // mode 变化:整个 view 都要换 if prefs.displayMode != currentMode { swapTickerView(to: prefs.displayMode) } + tickerView.showsIcon = prefs.showAppIcon + switch prefs.displayMode { + case .scroll, .scrollNoCode: + tickerView.preferredTotalWidth = prefs.scrollAutoWidth ? nil : CGFloat(prefs.scrollMenuBarWidth) + case .carousel: + tickerView.preferredTotalWidth = prefs.carouselAutoWidth ? nil : CGFloat(prefs.carouselMenuBarWidth) + case .compact: + tickerView.preferredTotalWidth = prefs.compactAutoWidth ? nil : CGFloat(prefs.compactMenuBarWidth) + case .minimal: + tickerView.preferredTotalWidth = nil + } // 各模式独立配置 if let scroll = tickerView as? TickerView { scroll.pixelsPerSecond = prefs.scrollSpeed.pixelsPerSecond @@ -75,6 +94,7 @@ final class StatusItemController { } if let compact = tickerView as? CompactTickerView { compact.scheme = prefs.colorScheme + compact.showsDirectionArrow = prefs.showDirectionArrow } if let minimal = tickerView as? MinimalTickerView { minimal.scheme = prefs.colorScheme @@ -88,7 +108,7 @@ final class StatusItemController { private static func makeView(for mode: TickerDisplayMode, scheme: TickerColorScheme) -> MenuBarTickerView { let frame = NSRect(x: 0, y: 0, width: 200, height: 22) switch mode { - case .scroll: + case .scroll, .scrollNoCode: return TickerView(frame: frame) case .carousel: return CarouselTickerView(frame: frame) @@ -107,6 +127,7 @@ final class StatusItemController { /// 通过 onContentChanged 回调把渲染好的 NSImage 设给 button.image。 private func swapTickerView(to mode: TickerDisplayMode) { tickerView.onContentChanged = nil + tickerView.invalidateAnimation() tickerView = Self.makeView(for: mode, scheme: prefs.colorScheme) wireUpTickerView() currentMode = mode @@ -154,29 +175,31 @@ final class StatusItemController { let image = tickerView.renderImage() button.image = image button.imagePosition = .imageOnly - statusItem.length = tickerView.totalWidth + statusItem.length = lockedPopoverLength ?? tickerView.totalWidth + } + + private func lockPopoverLength() { + guard lockedPopoverLength == nil else { return } + lockedPopoverLength = max(40, statusItem.length, tickerView.totalWidth) + statusItem.length = lockedPopoverLength ?? tickerView.totalWidth + } + + private func unlockPopoverLength() { + guard lockedPopoverLength != nil else { return } + lockedPopoverLength = nil + refreshButtonImage() } /// 各模式根据当前数据自己组装,写回到 statusItem.length。 private func render(quotes: [SymbolID: Quote]) { switch currentMode { - case .scroll: + case .scroll, .scrollNoCode: guard let view = tickerView as? TickerView else { return } let items = buildTickerItems(quotes: quotes) view.update(attributed: renderer.render(items: items)) case .carousel: guard let view = tickerView as? CarouselTickerView else { return } - // 汇总用 compact 简写格式拼成一条(「今 +¥8194 累 -¥29.6万 总 ¥64.9万」), - // 个股 / 指数各自单条。这样首屏看到的是简写总览,后续轮播看个股。 - var slots: [NSAttributedString] = [] - if let summary = buildCompactSummaryString() { - slots.append(summary) - } - let lineItems = buildLineItems(quotes: quotes) - for it in lineItems { - slots.append(renderer.render(items: [it])) - } - view.update(items: slots) + view.update(items: buildCarouselAttributedItems(quotes: quotes)) case .compact: guard let view = tickerView as? CompactTickerView else { return } let snap = refresher.snapshot @@ -192,7 +215,7 @@ final class StatusItemController { let snap = refresher.snapshot view.update(content: minimalContent(snap: snap, metric: prefs.minimalMetric)) } - statusItem.length = tickerView.totalWidth + statusItem.length = lockedPopoverLength ?? tickerView.totalWidth } private func minimalContent(snap: PortfolioSnapshot, metric: MinimalMetric) -> MinimalTickerView.Content? { @@ -354,6 +377,19 @@ final class StatusItemController { } } + /// Carousel 模式:汇总拼成一条,个股 / 指数各自单条。 + private func buildCarouselAttributedItems(quotes: [SymbolID: Quote]) -> [NSAttributedString] { + var slots: [NSAttributedString] = [] + if let summary = buildCompactSummaryString() { + slots.append(summary) + } + let lineItems = buildLineItems(quotes: quotes) + for it in lineItems { + slots.append(renderer.render(items: [it])) + } + return slots + } + /// 把 quotes / snapshot / 偏好聚合成 [TickerItem],scroll / carousel 共用。 /// compact / minimal 直接从 snapshot 拿数字,不走这里。 private func buildTickerItems(quotes: [SymbolID: Quote]) -> [TickerItem] { @@ -367,13 +403,6 @@ final class StatusItemController { String(format: " (%+.2f%%)", snap.todayPnLPct * 100) items.append(.summary(label: L("summary.today", comment: ""), value: value, direction: dir)) } - if prefs.showTotalAssets { - items.append(.summary( - label: L("summary.totalAssets", comment: ""), - value: snap.baseCurrency.format(snap.totalAssets), - direction: .neutral - )) - } if prefs.showAllTimePnL { let dir: TickerDirection = snap.allTimePnL > 0 ? .up : (snap.allTimePnL < 0 ? .down : .neutral) let sign = snap.allTimePnL >= 0 ? "+" : "-" @@ -381,6 +410,13 @@ final class StatusItemController { String(format: " (%+.2f%%)", snap.allTimePnLPct * 100) items.append(.summary(label: L("summary.allTime", comment: ""), value: value, direction: dir)) } + if prefs.showTotalAssets { + items.append(.summary( + label: L("summary.totalAssets", comment: ""), + value: snap.baseCurrency.format(snap.totalAssets), + direction: .neutral + )) + } let enabledIDs = prefs.tickerIndexIDs if !enabledIDs.isEmpty { @@ -448,6 +484,7 @@ final class StatusItemController { if popoverController.isShown { popoverController.close() } else { + lockPopoverLength() popoverController.show(relativeTo: button) } } @@ -468,6 +505,7 @@ final class StatusItemController { @objc private func showPopover() { guard let button = statusItem.button else { return } + lockPopoverLength() popoverController.show(relativeTo: button) } diff --git a/PanBar/MenuBar/TickerRenderer.swift b/PanBar/MenuBar/TickerRenderer.swift index 38f441f..6dda47a 100644 --- a/PanBar/MenuBar/TickerRenderer.swift +++ b/PanBar/MenuBar/TickerRenderer.swift @@ -20,6 +20,8 @@ enum TickerDirection { struct TickerRenderer { var scheme: TickerColorScheme = .east var font: NSFont = NSFont.menuBarFont(ofSize: 0) + var showsQuoteCode: Bool = true + var showsQuoteName: Bool = true /// 涨色 / 跌色:走 SemanticColors,与 Popover 保持一致 + 菜单栏对比度优化。 private var upColor: NSColor { SemanticColors.upNS(scheme: scheme) } @@ -29,13 +31,7 @@ struct TickerRenderer { /// 多项之间用 " · " 分隔。 func render(items: [TickerItem]) -> NSAttributedString { guard !items.isEmpty else { - return NSAttributedString( - string: L("ticker.empty", comment: "no quotes"), - attributes: [ - .font: font, - .foregroundColor: NSColor.secondaryLabelColor - ] - ) + return NSAttributedString() } let out = NSMutableAttributedString() @@ -137,8 +133,10 @@ struct TickerRenderer { let name = shortenedName(q.name, market: q.symbol.market) let s = NSMutableAttributedString() - s.append(NSAttributedString(string: display + " ", attributes: symbolAttr)) - if !name.isEmpty { + if showsQuoteCode { + s.append(NSAttributedString(string: display + " ", attributes: symbolAttr)) + } + if showsQuoteName && !name.isEmpty { s.append(NSAttributedString(string: name + " ", attributes: nameAttr)) } s.append(NSAttributedString(string: formatPrice(q.price) + " ", attributes: valueAttr)) diff --git a/PanBar/MenuBar/TickerView.swift b/PanBar/MenuBar/TickerView.swift index 8af47cb..63098a1 100644 --- a/PanBar/MenuBar/TickerView.swift +++ b/PanBar/MenuBar/TickerView.swift @@ -19,6 +19,8 @@ final class TickerView: NSView { private var lastTimestamp: CFTimeInterval = 0 /// 文字与图标之间的间距。 let iconWidth: CGFloat = 18 + var showsIcon: Bool = true + var preferredTotalWidth: CGFloat? /// 滚动文字可视区宽度(超出会被裁剪)。 var visibleTextWidth: CGFloat = 280 /// 隐私模式:屏幕共享或用户手动开启时,只显示图标和 "•••",不绘制具体行情。 @@ -38,7 +40,17 @@ final class TickerView: NSView { override var allowsVibrancy: Bool { false } var totalWidth: CGFloat { - iconWidth + 6 + visibleTextWidth + 4 + if attributed.length == 0 { + return leadingTextX + 4 + } + if let preferredTotalWidth { + return max(40, preferredTotalWidth) + } + return leadingTextX + visibleTextWidth + 4 + } + + private var leadingTextX: CGFloat { + showsIcon ? iconWidth + 6 : 2 } // MARK: lifecycle @@ -69,6 +81,11 @@ final class TickerView: NSView { func update(attributed: NSAttributedString) { self.attributed = attributed self.attributedWidth = attributed.size().width + if attributedWidth > 0 { + visibleTextWidth = min(520, max(20, attributedWidth + 8)) + } else { + visibleTextWidth = 0 + } if attributedWidth + loopGap > 0 { offset = offset.truncatingRemainder(dividingBy: attributedWidth + loopGap) if offset < 0 { offset += attributedWidth + loopGap } @@ -107,6 +124,10 @@ final class TickerView: NSView { displayLink = nil } + func invalidateAnimation() { + stopAnimation() + } + private func step(timestamp: CFTimeInterval) { defer { lastTimestamp = timestamp } if lastTimestamp == 0 { return } @@ -127,13 +148,14 @@ final class TickerView: NSView { override func draw(_ dirtyRect: NSRect) { let ctx = NSGraphicsContext.current?.cgContext - // P 图标(左侧) - drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth)) + if showsIcon { + drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth)) + } let textRect = NSRect( - x: iconWidth + 6, + x: leadingTextX, y: 0, - width: visibleTextWidth, + width: max(20, totalWidth - leadingTextX - 4), height: bounds.height ) diff --git a/PanBar/Popover/PopoverController.swift b/PanBar/Popover/PopoverController.swift index f4c5d74..0381998 100644 --- a/PanBar/Popover/PopoverController.swift +++ b/PanBar/Popover/PopoverController.swift @@ -9,6 +9,7 @@ final class PopoverController { /// 监听 popover 之外的点击,关 popover。`.transient` 行为对菜单栏 popover 有时漏 /// (尤其是点系统菜单栏 / 通知 / 其它 app 时),这里多加一层保险。 private var eventMonitor: Any? + var onClose: (() -> Void)? var isShown: Bool { popover.isShown } @@ -50,7 +51,9 @@ final class PopoverController { object: popover, queue: .main ) { [weak self] _ in - self?.handlePopoverClosed() + Task { @MainActor in + self?.handlePopoverClosed() + } } } @@ -62,15 +65,32 @@ final class PopoverController { private func handlePopoverClosed() { stopOutsideClickMonitor() refresher.setPopoverOpen(false) + onClose?() } - func show(relativeTo view: NSView) { + func show(relativeTo view: NSView, anchorWidth: CGFloat? = nil) { refresher.setPopoverOpen(true) refresher.refreshNow() - popover.show(relativeTo: view.bounds, of: view, preferredEdge: .minY) + popover.show(relativeTo: positioningRect(relativeTo: view, anchorWidth: anchorWidth), of: view, preferredEdge: .minY) startOutsideClickMonitor() } + private func positioningRect(relativeTo view: NSView, anchorWidth: CGFloat?) -> NSRect { + let rect: NSRect + if let anchorWidth { + let width = max(view.bounds.width, anchorWidth) + rect = NSRect( + x: view.bounds.maxX - width, + y: view.bounds.minY, + width: width, + height: view.bounds.height + ) + } else { + rect = view.bounds + } + return rect + } + func close() { stopOutsideClickMonitor() popover.performClose(nil) diff --git a/PanBar/Popover/Views/HoldingsTab.swift b/PanBar/Popover/Views/HoldingsTab.swift index 9c08fb2..fdcc2c9 100644 --- a/PanBar/Popover/Views/HoldingsTab.swift +++ b/PanBar/Popover/Views/HoldingsTab.swift @@ -139,6 +139,11 @@ private struct HoldingRow: View { return (q.price - holding.costPrice) * holding.quantity } + private var nativeTodayPnL: Decimal? { + guard let q = quote else { return nil } + return (q.price - q.prevClose) * holding.quantity + } + /// 右侧是否要显示「≈ 本位币」第三行。决定左侧布局是否要 Spacer 撑底。 private var hasBaseConversion: Bool { guard holding.currency != baseCurrency else { return false } @@ -146,7 +151,7 @@ private struct HoldingRow: View { } var body: some View { - HStack(alignment: .top) { + HStack(alignment: .top, spacing: 8) { VStack(alignment: .leading, spacing: 3) { HStack(spacing: 6) { Text(displayCode(holding.symbol)) @@ -177,6 +182,8 @@ private struct HoldingRow: View { .monospacedDigit() } .frame(maxHeight: hasBaseConversion ? .infinity : nil, alignment: .top) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + allTimeColumn Spacer() VStack(alignment: .trailing, spacing: 3) { HStack(spacing: 4) { @@ -192,33 +199,75 @@ private struct HoldingRow: View { .monospacedDigit() } } - if let pnl = nativePnL { - Text(signedPnL(pnl, currency: holding.currency)) - .font(.system(size: 11)) - .foregroundColor(pnlColor(pnl)) - .monospacedDigit() - } - // 本位币换算依赖 FX,只能从 snapshot 拿 - if holding.currency != baseCurrency, - let pos = position, let basePnL = pos.basePnL { - Text("≈ " + signedPnL(basePnL, currency: baseCurrency)) - .font(.system(size: 10)) - .foregroundColor(pnlColor(pos.pnl).opacity(0.7)) - .monospacedDigit() + if let pnl = nativeTodayPnL { + labeledPnL( + label: L("summary.today", comment: ""), + value: pnl, + currency: holding.currency, + fontSize: 11 + ) } } + .layoutPriority(2) } .padding(.horizontal, density.rowHorizontalPadding) .padding(.vertical, density.rowVerticalPadding) } + @ViewBuilder + private var allTimeColumn: some View { + VStack(alignment: .leading, spacing: 3) { + Text(L("summary.allTime", comment: "")) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.secondary) + if let pnl = nativePnL { + Text(signedPnL(pnl, currency: holding.currency)) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(pnlColor(pnl)) + .monospacedDigit() + .lineLimit(1) + .minimumScaleFactor(0.8) + } else { + Text("—") + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(.secondary) + .monospacedDigit() + } + // 本位币换算依赖 FX,只能从 snapshot 拿。 + if holding.currency != baseCurrency, + let pos = position, let basePnL = pos.basePnL { + Text("≈ " + signedPnL(basePnL, currency: baseCurrency)) + .font(.system(size: 10)) + .foregroundColor(pnlColor(pos.pnl).opacity(0.7)) + .monospacedDigit() + .lineLimit(1) + .minimumScaleFactor(0.8) + } + } + .frame(width: 74, alignment: .leading) + .layoutPriority(1) + } + + private func labeledPnL(label: String, value: Decimal, currency: Currency, fontSize: CGFloat) -> some View { + HStack(spacing: 3) { + Text(label) + .foregroundColor(.secondary) + Text(signedPnL(value, currency: currency)) + .foregroundColor(pnlColor(value)) + } + .font(.system(size: fontSize)) + .monospacedDigit() + .lineLimit(1) + .minimumScaleFactor(0.8) + } + private func displayCode(_ s: SymbolID) -> String { s.market == .us ? s.code.uppercased() : s.code } private var detailText: String { let qtyDisplay = "\(holding.quantity)" - let costDisplay = holding.currency.format(holding.costPrice) + let costDisplay = holding.currency.format(holding.costPrice, fractionDigits: 3) return String(format: L("holding.detail", comment: ""), qtyDisplay, costDisplay) } diff --git a/PanBar/Popover/Views/SummaryCards.swift b/PanBar/Popover/Views/SummaryCards.swift index d57d38c..91d8127 100644 --- a/PanBar/Popover/Views/SummaryCards.swift +++ b/PanBar/Popover/Views/SummaryCards.swift @@ -13,13 +13,6 @@ struct SummaryCards: View { tone: tone(for: snapshot.todayPnL), featured: false ) - card( - label: L("summary.totalAssets", comment: ""), - value: snapshot.baseCurrency.format(snapshot.totalAssets), - sub: "\(snapshot.baseCurrency.rawValue) · " + String(format: L("summary.positions", comment: ""), snapshot.positions.count), - tone: .neutral, - featured: true - ) card( label: L("summary.allTime", comment: ""), value: signed(snapshot.allTimePnL, currency: snapshot.baseCurrency), @@ -27,6 +20,13 @@ struct SummaryCards: View { tone: tone(for: snapshot.allTimePnL), featured: false ) + card( + label: L("summary.totalAssets", comment: ""), + value: snapshot.baseCurrency.format(snapshot.totalAssets), + sub: "\(snapshot.baseCurrency.rawValue) · " + String(format: L("summary.positions", comment: ""), snapshot.positions.count), + tone: .neutral, + featured: true + ) } } diff --git a/PanBar/Resources/Localizable.xcstrings b/PanBar/Resources/Localizable.xcstrings index 2db8ffd..cca3c77 100644 --- a/PanBar/Resources/Localizable.xcstrings +++ b/PanBar/Resources/Localizable.xcstrings @@ -1,1753 +1,5183 @@ { "sourceLanguage" : "zh-Hans", "strings" : { - "market.a" : { - "extractionState" : "manual", + "" : { + + }, + "--" : { + + }, + "-1.45%" : { + + }, + "—" : { + + }, + "%@ (%@)" : { "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "A-Share" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "A股" } } + "zh-Hans" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ (%2$@)" + } + } } }, - "market.hk" : { + "%@ → %@" : { "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Hong Kong" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "港股" } } + "zh-Hans" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ → %2$@" + } + } } }, - "market.us" : { + "%@%@" : { "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "United States" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "美股" } } + "zh-Hans" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@%2$@" + } + } } }, - "ticker.empty" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Loading…" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "加载中…" } } + "© 2026 PanBar contributors · MIT" : { + + }, + "+0.62%" : { + + }, + "about.reportIssue" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Report an Issue" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "反馈问题" + } + } } }, - "menu.refresh" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Refresh Now" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "立即刷新" } } + "about.tagline" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A lightweight macOS menu bar app for live stock quotes and portfolio P&L." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "macOS 菜单栏上的轻量盯盘工具" + } + } } }, - "menu.showPopover" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Show Popover" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示面板" } } + "action.add" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加" + } + } } }, - "menu.settings" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Settings…" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "设置…" } } + "action.cancel" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" + } + } } }, - "menu.quit" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Quit PanBar" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "退出 PanBar" } } + "action.delete" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除" + } + } } }, - "tab.holdings" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Holdings" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "持仓" } } + "action.dragToReorder" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Drag to reorder" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "拖动改顺序" + } + } } }, - "tab.watchlist" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Watchlist" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自选" } } + "action.edit" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑" + } + } } }, - "tab.indices" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Indices" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "大盘" } } + "action.exportCSV" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Export CSV" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出 CSV" + } + } } }, - "tab.alerts" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Alerts" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "预警" } } + "action.importCSV" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import CSV" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入 CSV" + } + } } }, - "summary.today" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Today" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "今日" } } + "action.later" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Later" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "稍后" + } + } } }, - "summary.totalAssets" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Total Assets" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "总资产" } } + "action.ok" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "好" + } + } } }, - "summary.allTime" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "All-Time" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "累计" } } + "action.openInBrowser" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open in browser" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在浏览器中打开" + } + } } }, - "summary.positions" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "%d positions" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%d 只持仓" } } + "action.retry" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重试" + } + } } }, - "popover.subtitle.markets" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "%d markets" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%d 个市场" } } + "action.save" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存" + } + } } }, - "popover.subtitle.holdings" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "%d holdings" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%d 只持仓" } } + "alert.body.changePctAbove" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change %@ ≥ %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日 %@ ≥ %@" + } + } } }, - "footer.updated" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Updated %ds ago" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%d 秒前更新" } } + "alert.body.changePctBelow" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change %@ ≤ %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日 %@ ≤ %@" + } + } } }, - "footer.loading" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Loading…" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "加载中…" } } + "alert.body.priceAbove" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Price %@ ≥ %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前价 %@ ≥ %@" + } + } } }, - "footer.refreshing" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Refreshing…" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在刷新…" } } + "alert.body.priceBelow" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Price %@ ≤ %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前价 %@ ≤ %@" + } + } } }, - "market.status.open" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Open" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "开盘" } } + "alert.cond.changePctAbove" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change % ≥" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "涨跌幅 ≥" + } + } } }, - "market.status.lunch" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Lunch" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "午休" } } + "alert.cond.changePctBelow" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change % ≤" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "涨跌幅 ≤" + } + } } }, - "market.status.closed" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Closed" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "休市" } } + "alert.cond.priceAbove" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Price ≥" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "价格 ≥" + } + } } }, - "market.tooltip.openSettings" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Click to manage hours & overrides" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "点击进入设置 → 市场" } } + "alert.cond.priceBelow" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Price ≤" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "价格 ≤" + } + } } }, - "footer.github.tooltip" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Source on GitHub" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "项目主页 (GitHub)" } } + "alert.lastTriggered" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Triggered %@ ago" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@前触发" + } + } } }, - "footer.donate.tooltip" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Buy me a coffee" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "捐赠支持开发" } } + "alert.logic.and" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "AND" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "同时满足 AND" + } + } } }, - "ticker.displayMode" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Display mode" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "展现形式" } } + "alert.logic.or" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "OR" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "任一满足 OR" + } + } } }, - "displayMode.scroll" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Scroll (horizontal)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滚动(横向)" } } + "alert.pause" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pause" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "暂停" + } + } } }, - "displayMode.carousel" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Carousel (one at a time)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "轮播(一条一条)" } } + "alert.resume" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resume" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用" + } + } } }, - "displayMode.compact" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Fixed (compact)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "固定(简写)" } } + "alerts.addFirst" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ Add Alert" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ 添加预警" + } + } } }, - "displayMode.minimal" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Minimal (single metric)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "极简(单数字)" } } + "alerts.addTitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add Alert" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加预警" + } + } } }, - "displayMode.scroll.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "All items scroll horizontally like a stock ticker." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "经典左右滚动,跟港交所大屏一个风格。" } } + "alerts.advanced" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advanced" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "高级" + } + } } }, - "displayMode.carousel.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Shows one item at a time, fades to the next every few seconds. Easier on the eyes than continuous scroll." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每条停留几秒后滑入下一条,比连续滚动眼花少。" } } + "alerts.advanced.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daily cap resets at midnight (CN time). Trading-hours-only checks if any of A/HK/US market is open." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日次数上限按北京时间凌晨重置。仅交易时段=任一 A/港/美 市场开盘即可。" + } + } } }, - "displayMode.compact.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Three abbreviated numbers (today / total P&L / assets) with K/M suffix. No animation." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "三个简写数字(今日 / 累计 / 总市值),K/M 后缀,不动。" } } + "alerts.col.todayCount" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Today" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日已触发" + } + } } }, - "displayMode.minimal.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "A single number of your choice + arrow indicator. Smallest footprint." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "只显示一个你选的数字 + ↑↓ 箭头,菜单栏最不占地方。" } } + "alerts.cooldown" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cooldown (seconds)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "冷却时间(秒)" + } + } } }, - "ticker.minimalMetric" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Show metric" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示指标" } } + "alerts.dailyCap" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Max triggers per day" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日最多触发次数" + } + } } }, - "compact.label.today" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "TODAY" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "今" } } + "alerts.editTitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit Alert" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑预警" + } + } } }, - "compact.label.allTime" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "P&L" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "累" } } + "alerts.empty" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No alerts yet." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "还没有预警" + } + } } }, - "compact.label.total" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "ASSETS" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "总" } } + "alerts.logic" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Combine" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "组合方式" + } + } } }, - "update.available.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Update available — %@" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "有新版本可用 — %@" } } + "alerts.permission.denied.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PanBar can't push alerts. Re-enable notifications in System Settings → Notifications → PanBar. As a fallback, alerts will surface as a dialog." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "PanBar 无法推送预警通知。请到 系统设置 → 通知 → PanBar 中重新开启。无法推送时会用对话框兜底。" + } + } } }, - "update.available.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Current: %@\nLatest: %@" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当前版本:%@\n最新版本:%@" } } + "alerts.permission.denied.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notifications are blocked" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通知已被系统禁止" + } + } } }, - "update.action.download" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Download" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "去下载" } } + "alerts.permission.notDetermined" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notification permission not granted yet." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚未授权 PanBar 发送通知。" + } + } } }, - "update.action.later" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Later" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "稍后" } } + "alerts.permission.request" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请求授权" + } + } } }, - "update.uptodate.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Already up to date" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已是最新版本" } } + "alerts.plannedP2" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerts arrive in Phase 2" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "价格预警将在 Phase 2 上线" + } + } } }, - "update.uptodate.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "You're on PanBar %@, the latest available." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当前 PanBar %@ 已经是最新版。" } } + "alerts.primary" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Primary Condition" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "主条件" + } + } } }, - "update.error.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Update check failed" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "检查更新失败" } } + "alerts.secondary" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secondary Condition" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "副条件" + } + } } }, - "update.error.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Could not reach GitHub. Check your network and try again." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无法连接 GitHub,请检查网络后重试。" } } + "alerts.secondary.disabled" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable to combine two conditions with AND / OR." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用后可以再加一条,与主条件用 AND / OR 组合。" + } + } } }, - "settings.markets" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Markets" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "市场" } } + "alerts.secondary.enable" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用" + } + } } }, - "market.refreshSection" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Refresh" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新" } } + "alerts.testNotification" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Test" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "测试通知" + } + } } }, - "market.quoteInterval" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Quote refresh interval" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "行情刷新间隔" } } + "alerts.thresholdPct" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Threshold (%, e.g. 5 for ±5%)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "阈值(%,如填 5 表示 ±5%)" + } + } } }, - "market.quoteInterval.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Used while markets are open. When the popover is showing, refresh stays at 3s." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "开盘期间用此间隔刷新。popover 打开时固定 3s。" } } + "alerts.thresholdPrice" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Threshold (price)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "阈值(价格)" + } + } } }, - "market.pauseWhenClosed" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Pause auto-refresh when all markets closed" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "全市场休市时暂停自动刷新" } } + "alerts.tradingHoursOnly" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trading hours only" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "仅交易时段" + } + } } }, - "market.quoteInterval.3s" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 3 seconds (fast)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 3 秒(快)" } } + "alerts.weekdaysOnly" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weekdays only" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "仅工作日" + } + } } }, - "market.quoteInterval.5s" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 5 seconds (default)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 5 秒(默认)" } } + "backup.confirmReplace.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The backup contains %d holdings, %d watchlist items, %d alerts and %d settings. All current data will be wiped and replaced." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "备份文件包含 %d 条持仓、%d 条自选、%d 条预警、%d 项设置。当前数据将被完全清空并替换。" + } + } } }, - "market.quoteInterval.10s" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 10 seconds" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 10 秒" } } + "backup.confirmReplace.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Replace current data?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "替换当前所有数据?" + } + } } }, - "market.quoteInterval.30s" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 30 seconds" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 30 秒" } } + "backup.confirmReplace.yes" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Replace All" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "替换" + } + } } }, - "market.quoteInterval.60s" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every minute (saves bandwidth)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 1 分钟(省流量)" } } + "backup.error.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Backup operation failed" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "备份操作失败" + } + } } }, - "market.hoursSection" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Trading hours & overrides" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "交易时段 + 节假日纠错" } } + "backup.exportAll" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Export All" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出全部" + } + } } }, - "market.hours.a" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "09:30–11:30, 13:00–15:00 Asia/Shanghai · Mon–Fri" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "09:30–11:30、13:00–15:00 北京时间 · 周一至周五" } } + "backup.exported.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exported %d holdings, %d watchlist items, %d alerts and %d settings." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已导出 %d 条持仓、%d 条自选、%d 条预警、%d 项设置。" + } + } } }, - "market.hours.hk" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "09:30–12:00, 13:00–16:00 Asia/Hong_Kong · Mon–Fri" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "09:30–12:00、13:00–16:00 香港时间 · 周一至周五" } } + "backup.exported.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Export complete" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出完成" + } + } } }, - "market.hours.us" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "09:30–16:00 America/New_York · Mon–Fri (no lunch break)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "09:30–16:00 纽约时间 · 周一至周五(无午休)" } } + "backup.exportTitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Export PanBar backup" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出 PanBar 备份" + } + } } }, - "market.hours.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Holiday detection isn't reliable (no free, accurate API). When PanBar gets it wrong on a holiday or special trading day, use the override below — it auto-clears at the next market-local midnight." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "节假日判断没有可靠的免费 API,可能出错。如果某天判断不对(节日开盘 / 工作日休市),用下面的「今天强制开/关」临时纠错,过了当地零点自动失效。" } } + "backup.importAll" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入…" + } + } } }, - "market.override.label" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Today" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "今天" } } + "backup.imported.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restored %d holdings, %d watchlist items, %d alerts, %d settings." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已恢复 %d 条持仓、%d 条自选、%d 条预警、%d 项设置。" + } + } } }, - "market.override.auto" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Auto (follow weekday rules)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自动(按工作日规则)" } } + "backup.imported.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import complete" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入完成" + } + } } }, - "market.override.forceOpen" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Force open today" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "今天强制开盘" } } + "backup.importTitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import PanBar backup" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入 PanBar 备份" + } + } } }, - "market.override.forceClosed" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Force closed today" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "今天强制休市" } } + "backup.revealInFinder" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show in Finder" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在访达中显示" + } + } } }, - "footer.cached" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Cached data, refreshing…" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "缓存数据,刷新中…" } } + "col.active" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Active" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用" + } + } } }, - "footer.offline" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Offline" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "离线" } } + "col.condition" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Condition" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "条件" + } + } } }, - "holdings.empty" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "No holdings yet." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "还没有持仓" } } + "col.cost" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cost" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "成本" + } + } } }, - "holdings.addFirst" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "+ Add Position" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "+ 添加持仓" } } + "col.inTicker" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu Bar" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "菜单栏" + } + } } }, - "holdings.quickAdd" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Add Position" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加持仓" } } + "col.inTicker.help" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show this item in the menu bar scroller" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "是否在菜单栏滚动条中展示这条" + } + } } }, - "watchlist.quickAdd" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Add to Watchlist" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加自选" } } + "col.market" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Market" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "市场" + } + } } }, - "holdings.addTitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Add Position" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加持仓" } } + "col.name" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "名称" + } + } } }, - "holding.detail" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "%@ sh · cost %@" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 股 · 成本 %@" } } + "col.qty" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quantity" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数量" + } + } } }, - "watchlist.empty" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "No watchlist items." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "还没有自选股" } } + "col.symbol" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Symbol" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "代码" + } + } } }, - "watchlist.addTitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Add to Watchlist" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加自选" } } + "col.threshold" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Threshold" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "阈值" + } + } } }, "comingSoon" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Coming Soon" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "即将推出" } } + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coming Soon" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "即将推出" + } + } } }, - "indices.plannedP2" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Indices arrive in Phase 2" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "大盘指数将在 Phase 2 上线" } } + "compact.label.allTime" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "P&L" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "累" + } + } } }, - "alerts.plannedP2" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Alerts arrive in Phase 2" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "价格预警将在 Phase 2 上线" } } + "compact.label.today" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TODAY" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今" + } + } } }, - "action.add" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Add" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加" } } + "compact.label.total" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "ASSETS" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "总" + } + } } }, - "action.save" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Save" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "保存" } } + "csv.exportTitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Export to CSV" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出为 CSV" + } + } } }, - "action.cancel" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Cancel" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "取消" } } + "csv.importTitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import from CSV" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "从 CSV 导入" + } + } } }, - "action.delete" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Delete" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除" } } + "dataSource.finnhub.help" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Free tier: 60 req/min for US stocks. Get an API key at finnhub.io." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "免费档 60 次/分钟,仅美股。前往 finnhub.io 注册获取 Key。" + } + } } }, - "action.edit" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Edit" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "编辑" } } + "dataSource.finnhub.keyPlaceholder" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paste your Finnhub API key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "粘贴 Finnhub API Key" + } + } } }, - "holdings.editTitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Edit Position" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "编辑持仓" } } + "dataSource.finnhub.signup" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Get key →" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "去注册 →" + } + } } }, - "watchlist.editTitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Edit Watchlist Item" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "编辑自选" } } + "dataSource.usOnly" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "US only" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "仅美股" + } + } } }, - "col.inTicker" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Ticker" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滚动" } } + "delete.confirm.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This action cannot be undone." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此操作无法撤销。" + } + } } }, - "col.inTicker.help" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Show this item in the menu bar scroller" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "是否在菜单栏滚动条中展示这条" } } + "delete.confirm.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete %@?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "确定删除「%@」?" + } + } } }, - "ticker.summarySection" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Summary metrics" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "汇总指标" } } + "density.compact" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compact" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "紧凑" + } + } } }, - "ticker.showTodayPnL" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Show today's P&L" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示今日盈亏" } } + "density.spacious" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spacious" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "宽松" + } + } } }, - "ticker.showTotalAssets" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Show total assets" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示当前市值" } } + "density.standard" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standard" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "标准" + } + } } }, - "ticker.showAllTimePnL" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Show all-time P&L" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示累计盈亏" } } + "displayMode.carousel" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carousel (one at a time)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "轮播(一条一条)" + } + } } }, - "ticker.summaryHint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Applies to all display modes (scroll / carousel / fixed). Fixed mode shows only these, no individual quotes." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "对全部展现形式生效(滚动 / 轮播 / 固定)。固定模式只显示这几项,不展示个股。" } } + "displayMode.carousel.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shows one item at a time, fades to the next every few seconds. Easier on the eyes than continuous scroll." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每条停留几秒后滑入下一条,比连续滚动眼花少。" + } + } } }, - "ticker.itemsHint.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Pick stocks per row" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "单只勾选" } } + "displayMode.compact" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fixed (compact)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "固定(简写)" + } + } } }, - "ticker.itemsHint.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Each holding and watchlist row has a \"Ticker\" switch — go to Settings → Portfolio / Watchlist to enable or disable individual items." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "持仓和自选每一行都有\"滚动\"开关 — 到 设置 → 持仓 / 自选 单独开关每一条。" } } + "displayMode.compact.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fixed summary metrics with abbreviated numbers. No animation." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "固定显示勾选的汇总指标,K/M 简写,不滚动。" + } + } } }, - "col.symbol" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Symbol" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "代码" } } + "displayMode.minimal" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minimal (single metric)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "极简(单数字)" + } + } } }, - "col.name" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Name" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "名称" } } + "displayMode.minimal.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A single number of your choice + arrow indicator. Smallest footprint." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "只显示一个你选的数字 + ↑↓ 箭头,菜单栏最不占地方。" + } + } } }, - "col.market" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Market" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "市场" } } + "displayMode.scroll" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scroll (horizontal)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "滚动(横向)" + } + } } }, - "col.qty" : { + "displayMode.scroll.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "All items scroll horizontally like a stock ticker." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "经典左右滚动,跟港交所大屏一个风格。" + } + } + } + }, + "displayMode.scrollNoCode" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scroll (no symbol code)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "滚动(不带代码)" + } + } + } + }, + "displayMode.scrollNoCode.hint" : { + "extractionState" : "manual", "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Quantity" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "数量" } } + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scrolls horizontally like the classic mode, but stock items show only name, price, and change." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "跟经典滚动一样,但个股只显示名称、价格和涨跌幅,不显示股票代码。" + } + } } }, - "col.cost" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Cost" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "成本" } } + "error.codeRequired" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Symbol code is required." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请填写代码" + } + } } }, - "settings.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "PanBar Settings" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "PanBar 设置" } } + "error.costInvalid" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cost must be a positive number." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "成本必须是正数" + } + } } }, - "settings.general" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "General" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "通用" } } + "error.qtyInvalid" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quantity must be a positive number." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数量必须是正数" + } + } } }, - "settings.portfolio" : { + "error.thresholdInvalid" : { + "extractionState" : "stale", "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Portfolio" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "持仓" } } + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Threshold must be a number." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "阈值必须是数字" + } + } } }, - "settings.watchlist" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Watchlist" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自选" } } + "Finnhub" : { + + }, + "footer.cached" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cached data, refreshing…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "缓存数据,刷新中…" + } + } } }, - "settings.about" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "About" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关于" } } + "footer.donate.tooltip" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buy me a coffee" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "捐赠支持开发" + } + } } }, - "settings.launchAtLogin" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Launch at login" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "开机启动" } } + "footer.github.tooltip" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Source on GitHub" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "项目主页 (GitHub)" + } + } } }, - "settings.baseCurrency" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Base Currency" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "本位币" } } + "footer.loading" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loading…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载中…" + } + } } }, - "about.tagline" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "A lightweight macOS menu bar app for live stock quotes and portfolio P&L." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "macOS 菜单栏上的轻量盯盘工具" } } + "footer.offline" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Offline" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "离线" + } + } } }, - "error.codeRequired" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Symbol code is required." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "请填写代码" } } + "footer.refreshing" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refreshing…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在刷新…" + } + } } }, - "error.qtyInvalid" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Quantity must be a positive number." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "数量必须是正数" } } + "footer.updated" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Updated %ds ago" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d 秒前更新" + } + } } }, - "error.costInvalid" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Cost must be a positive number." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "成本必须是正数" } } + "fx.autoRefresh" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto-refresh" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动刷新" + } + } } }, - "error.thresholdInvalid" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Threshold must be a number." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阈值必须是数字" } } + "fx.autoRefresh.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rates only change a few cents per day for most currencies — 1h is plenty. Set to Off if you'd rather refresh manually." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "汇率变化通常很小,1 小时一次足够。也可以关掉改为手动刷新。" + } + } } }, - "settings.ticker" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Ticker" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滚动" } } + "fx.autoRefresh.interval" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Interval" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "刷新间隔" + } + } } }, - "settings.alerts" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Alerts" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "预警" } } + "fx.empty" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No rates yet. Tap Refresh to load." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "还没有汇率数据,点「立即刷新」加载。" + } + } } }, - "settings.colorScheme" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Color Scheme" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "涨跌配色" } } + "fx.interval.1d" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every day" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每天" + } + } } }, - "settings.scrollSpeed" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Scroll Speed" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滚动速度" } } + "fx.interval.1h" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every hour" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 1 小时" + } + } } }, - "settings.pauseOnHover" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Pause on hover" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Hover 时暂停" } } + "fx.interval.5min" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 5 minutes" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 5 分钟" + } + } } }, - "settings.pauseWhenClosed" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Pause when all markets closed" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "全部市场休市时暂停" } } + "fx.interval.6h" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 6 hours" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 6 小时" + } + } } }, - "settings.maxItems" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Max items in ticker: %d" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滚动条最多展示数: %d" } } + "fx.interval.15min" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 15 minutes" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 15 分钟" + } + } } }, - "speed.slow" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Slow" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "慢" } } + "fx.interval.off" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Off" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭" + } + } } }, - "speed.medium" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Medium" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "中" } } + "fx.lastFetched" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Last fetched %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上次拉取于 %@" + } + } } }, - "speed.fast" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Fast" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "快" } } + "fx.neverFetched" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Never fetched" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚未拉取" + } + } } }, - "scheme.east" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "East (red up, green down)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "东方 红涨绿跌" } } + "fx.refresh.failed" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refresh failed — check network or data source" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "刷新失败 — 检查网络或数据源" + } + } } }, - "scheme.west" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "West (green up, red down)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "西方 绿涨红跌" } } + "fx.refresh.success" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Updated %d rates" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已更新 %d 对汇率" + } + } } }, - "scheme.mono" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Monochrome" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "黑白" } } + "fx.refreshNow" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refresh now" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "立即刷新" + } + } } }, - "alert.cond.priceAbove" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Price ≥" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "价格 ≥" } } + "fx.source" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data Source" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据源" + } + } } }, - "alert.cond.priceBelow" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Price ≤" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "价格 ≤" } } + "fx.source.eastmoney" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "EastMoney (USDCNY / HKDCNY). Other directions are derived via CNY." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "东方财富(USDCNY / HKDCNY)。其它方向以 CNY 为枢轴换算。" + } + } } }, - "alert.cond.changePctAbove" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Change % ≥" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "涨跌幅 ≥" } } + "fx.time.days" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%dd ago" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d 天前" + } + } } }, - "alert.cond.changePctBelow" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Change % ≤" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "涨跌幅 ≤" } } + "fx.time.hours" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%dh ago" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d 小时前" + } + } } }, - "alert.body.priceAbove" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Price %@ ≥ %@" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当前价 %@ ≥ %@" } } + "fx.time.minutes" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%dm ago" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d 分钟前" + } + } } }, - "alert.body.priceBelow" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Price %@ ≤ %@" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当前价 %@ ≤ %@" } } + "fx.time.seconds" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%ds ago" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d 秒前" + } + } } }, - "alert.body.changePctAbove" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Change %@ ≥ %@" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "今日 %@ ≥ %@" } } + "holding.detail" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ sh · cost %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 股 · 成本 %@" + } + } } }, - "alert.body.changePctBelow" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Change %@ ≤ %@" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "今日 %@ ≤ %@" } } + "holdings.addFirst" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ Add Position" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ 添加持仓" + } + } } }, - "alert.lastTriggered" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Triggered %@ ago" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@前触发" } } + "holdings.addTitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add Position" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加持仓" + } + } } }, - "alert.pause" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Pause" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "暂停" } } + "holdings.editTitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit Position" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑持仓" + } + } } }, - "alert.resume" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Resume" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用" } } + "holdings.empty" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No holdings yet." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "还没有持仓" + } + } } }, - "alerts.empty" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "No alerts yet." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "还没有预警" } } + "holdings.quickAdd" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add Position" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加持仓" + } + } } }, - "alerts.addFirst" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "+ Add Alert" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "+ 添加预警" } } + "hotkey.recording" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Press shortcut…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "按下新组合…" + } + } } }, - "alerts.addTitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Add Alert" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加预警" } } + "hotkey.resetDefault" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset to default" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重置为默认" + } + } } }, - "alerts.editTitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Edit Alert" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "编辑预警" } } + "hotkey.togglePopover" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show / hide popover" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "唤起 / 收起面板" + } + } } }, - "alerts.cooldown" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Cooldown (seconds)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "冷却时间(秒)" } } + "hotkey.togglePrivacy" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toggle privacy hide" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换隐私隐藏" + } + } } }, - "alerts.thresholdPct" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Threshold (%, e.g. 5 for ±5%)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阈值(%,如填 5 表示 ±5%)" } } + "hotkey.unbound" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to set" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点击设置" + } + } } }, - "alerts.thresholdPrice" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Threshold (price)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阈值(价格)" } } + "indices.empty" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loading indices…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "指数加载中…" + } + } } }, - "col.condition" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Condition" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "条件" } } + "indices.failed" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to load indices" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "指数加载失败" + } + } } }, - "col.threshold" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Threshold" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阈值" } } + "indices.plannedP2" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indices arrive in Phase 2" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大盘指数将在 Phase 2 上线" + } + } } }, - "col.active" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Active" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用" } } + "language.auto" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Follow System" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "跟随系统" + } + } } }, - "indices.empty" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Loading indices…" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "指数加载中…" } } + "language.restart.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Language changes take effect after relaunching the app. Relaunch now?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语言切换需要重启 PanBar 才能完整生效。是否现在重启?" + } + } } }, - "indices.failed" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Failed to load indices" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "指数加载失败" } } + "language.restart.now" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Relaunch Now" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "立即重启" + } + } } }, - "action.retry" : { + "language.restart.title" : { + "extractionState" : "stale", "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Retry" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重试" } } + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restart required" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要重启 PanBar" + } + } } }, - "search.placeholder" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Search: code / name / pinyin (e.g. 600519, AAPL, mtgz)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "搜索:代码 / 名称 / 拼音(如 600519、AAPL、mtgz)" } } + "Live" : { + + }, + "loading" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loading…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载中…" + } + } } }, - "search.noResults" : { + "market.a" : { + "extractionState" : "manual", "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "No matches" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无匹配结果" } } + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A-Share" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "A股" + } + } } }, - "settings.privacySection" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Privacy" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "隐私" } } + "market.hk" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hong Kong" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "港股" + } + } } }, - "settings.hideOnScreenShare" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Auto-hide ticker during screen sharing" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "屏幕共享时自动隐藏滚动" } } + "market.hours.a" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "09:30–11:30, 13:00–15:00 Asia/Shanghai · Mon–Fri" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "09:30–11:30、13:00–15:00 北京时间 · 周一至周五" + } + } } }, - "settings.hideOnScreenShare.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "macOS has no public API to detect ongoing screen recording. We scan for known sharing indicator windows (Zoom toolbar, Teams floating bar, etc.) — best effort, may miss some. For reliable hide use ⌘⌃M." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "macOS 没有公开 API 让 app 检测「正在录屏」。这里通过扫描 Zoom / Teams 等共享时才会出现的浮动窗口来识别,best-effort 可能漏报。需要 100% 可靠时用 ⌘⌃M 手动切换。" } } + "market.hours.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Holiday detection isn't reliable (no free, accurate API). When PanBar gets it wrong on a holiday or special trading day, use the override below — it auto-clears at the next market-local midnight." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "节假日判断没有可靠的免费 API,可能出错。如果某天判断不对(节日开盘 / 工作日休市),用下面的「今天强制开/关」临时纠错,过了当地零点自动失效。" + } + } } }, - "menu.privacy.hideNow" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Hide ticker (privacy)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "隐藏滚动(隐私)" } } + "market.hours.hk" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "09:30–12:00, 13:00–16:00 Asia/Hong_Kong · Mon–Fri" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "09:30–12:00、13:00–16:00 香港时间 · 周一至周五" + } + } } }, - "settings.hotkeysSection" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Hotkeys" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "全局快捷键" } } + "market.hours.us" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "09:30–16:00 America/New_York · Mon–Fri (no lunch break)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "09:30–16:00 纽约时间 · 周一至周五(无午休)" + } + } } }, - "settings.hotkeys.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Click a field, then press the new shortcut. Press ⌫ to clear, ⎋ to cancel." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "点击下面任一框,按下新组合即可录入。⌫ 清除,⎋ 取消。" } } + "market.hoursSection" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trading hours & overrides" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "交易时段 + 节假日纠错" + } + } } }, - "hotkey.togglePopover" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Show / hide popover" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "唤起 / 收起面板" } } + "market.override.auto" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto (follow weekday rules)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动(按工作日规则)" + } + } } }, - "hotkey.togglePrivacy" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Toggle privacy hide" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "切换隐私隐藏" } } + "market.override.forceClosed" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Force closed today" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今天强制休市" + } + } } }, - "hotkey.recording" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Press shortcut…" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "按下新组合…" } } + "market.override.forceOpen" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Force open today" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今天强制开盘" + } + } } }, - "hotkey.unbound" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Click to set" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "点击设置" } } + "market.override.label" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Today" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今天" + } + } } }, - "hotkey.resetDefault" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Reset to default" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重置为默认" } } + "market.pauseWhenClosed" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pause auto-refresh when all markets closed" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全市场休市时暂停自动刷新" + } + } } }, - "settings.backupSection" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Backup & Restore" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "备份与恢复" } } + "market.quoteInterval" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quote refresh interval" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "行情刷新间隔" + } + } } }, - "settings.backup.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Exports everything (holdings / watchlist / alerts / settings / hotkeys / data sources / Finnhub key) to a single JSON file. Importing REPLACES all current data after confirmation." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出包含所有持仓 / 自选 / 预警 / 设置(语言 / 主题 / 配色 / 快捷键 / 数据源优先级 / Finnhub Key 等)为一个 JSON 文件。导入会先二次确认,然后整体覆盖当前数据。" } } + "market.quoteInterval.3s" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 3 seconds (fast)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 3 秒(快)" + } + } } }, - "backup.exportAll" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Export All" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出全部" } } + "market.quoteInterval.5s" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 5 seconds (default)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 5 秒(默认)" + } + } } }, - "backup.importAll" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Import…" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导入…" } } + "market.quoteInterval.10s" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 10 seconds" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 10 秒" + } + } } }, - "backup.exportTitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Export PanBar backup" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出 PanBar 备份" } } + "market.quoteInterval.30s" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 30 seconds" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 30 秒" + } + } } }, - "backup.importTitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Import PanBar backup" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导入 PanBar 备份" } } + "market.quoteInterval.60s" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every minute (saves bandwidth)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 1 分钟(省流量)" + } + } } }, - "backup.confirmReplace.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Replace current data?" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "替换当前所有数据?" } } + "market.quoteInterval.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Used while markets are open. When the popover is showing, refresh stays at 3s." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "开盘期间用此间隔刷新。popover 打开时固定 3s。" + } + } } }, - "backup.confirmReplace.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "The backup contains %d holdings, %d watchlist items, %d alerts and %d settings. All current data will be wiped and replaced." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "备份文件包含 %d 条持仓、%d 条自选、%d 条预警、%d 项设置。当前数据将被完全清空并替换。" } } + "market.refreshSection" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refresh" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "刷新" + } + } } }, - "backup.confirmReplace.yes" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Replace All" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "替换" } } + "market.status.closed" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Closed" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "休市" + } + } } }, - "backup.imported.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Import complete" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导入完成" } } + "market.status.lunch" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lunch" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "午休" + } + } } }, - "backup.imported.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Restored %d holdings, %d watchlist items, %d alerts, %d settings." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已恢复 %d 条持仓、%d 条自选、%d 条预警、%d 项设置。" } } + "market.status.open" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "开盘" + } + } } }, - "backup.error.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Backup operation failed" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "备份操作失败" } } + "market.tooltip.openSettings" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to manage hours & overrides" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点击进入设置 → 市场" + } + } } }, - "backup.exported.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Export complete" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出完成" } } + "market.us" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "United States" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "美股" + } + } } }, - "backup.exported.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Exported %d holdings, %d watchlist items, %d alerts and %d settings." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已导出 %d 条持仓、%d 条自选、%d 条预警、%d 项设置。" } } + "menu.checkForUpdates" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check for Updates" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检查更新" + } + } } }, - "backup.revealInFinder" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Show in Finder" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在访达中显示" } } + "menu.privacy.hideNow" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hide ticker (privacy)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐藏滚动(隐私)" + } + } } }, "menu.privacy.show" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Show ticker" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示滚动" } } + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show ticker" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示滚动" + } + } } }, - "ticker.indicesSection" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Indices in Ticker" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滚动条 · 大盘指数" } } + "menu.quit" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quit PanBar" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "退出 PanBar" + } + } } }, - "ticker.indicesHint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Pick which indices appear in the menu bar scroller. Refreshed every 15s." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "勾选哪些大盘指数显示在菜单栏滚动条。15 秒刷新一次。" } } + "menu.refresh" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refresh Now" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "立即刷新" + } + } } }, - "ticker.holdingsSection" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Holdings in Ticker" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滚动条 · 持仓" } } + "menu.settings" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Settings…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置…" + } + } } }, - "ticker.watchlistSection" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Watchlist in Ticker" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滚动条 · 自选" } } + "menu.showPopover" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show Popover" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示面板" + } + } } }, - "settings.language" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Language" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "语言" } } + "notification.fallbackHint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "(Shown as a fallback dialog because system notifications are disabled for PanBar.)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "(系统通知被禁用,所以这里用对话框兜底)" + } + } } }, - "language.auto" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Follow System" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "跟随系统" } } + "notification.openSettings" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open System Settings" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开系统设置" + } + } } }, - "language.restart.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Restart required" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "需要重启 PanBar" } } + "notification.test.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you see this, alerts will work. If not, check System Settings → Notifications." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "看到这条说明预警会正常推送。看不到请去 系统设置 → 通知 检查 PanBar 是否被允许。" + } + } } }, - "language.restart.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Language changes take effect after relaunching the app. Relaunch now?" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "语言切换需要重启 PanBar 才能完整生效。是否现在重启?" } } + "notification.test.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PanBar test notification" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "PanBar 测试通知" + } + } } }, - "language.restart.now" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Relaunch Now" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "立即重启" } } + "onboarding.add.added" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Added!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已添加" + } + } } }, - "action.later" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Later" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "稍后" } } + "onboarding.add.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pick a market and enter a code. You can add more later in Settings." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择市场、填入代码即可。之后可在设置里继续添加。" + } + } } }, - "loading" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Loading…" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "加载中…" } } + "onboarding.add.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add your first stock" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加你的第一只股票" + } + } } }, - "action.ok" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "好" } } + "onboarding.add.toWatchlist" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add to Watchlist" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加到自选" + } + } } }, - "alerts.testNotification" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Test" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "测试通知" } } + "onboarding.back" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Back" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上一步" + } + } } }, - "notification.test.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "PanBar test notification" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "PanBar 测试通知" } } + "onboarding.color.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "East: red up / green down. West: green up / red down. Mono: high-contrast." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "东方红涨绿跌,西方绿涨红跌,黑白则不分颜色。" + } + } } }, - "notification.test.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "If you see this, alerts will work. If not, check System Settings → Notifications." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "看到这条说明预警会正常推送。看不到请去 系统设置 → 通知 检查 PanBar 是否被允许。" } } + "onboarding.color.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pick a color scheme" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选个涨跌配色" + } + } } }, - "notification.fallbackHint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "(Shown as a fallback dialog because system notifications are disabled for PanBar.)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "(系统通知被禁用,所以这里用对话框兜底)" } } + "onboarding.finish" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finish" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成" + } + } } }, - "notification.openSettings" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Open System Settings" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "打开系统设置" } } + "onboarding.next" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Next" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一步" + } + } } }, - "alerts.permission.denied.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Notifications are blocked" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "通知已被系统禁止" } } + "onboarding.skip" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skip" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "跳过" + } + } } }, - "alerts.permission.denied.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "PanBar can't push alerts. Re-enable notifications in System Settings → Notifications → PanBar. As a fallback, alerts will surface as a dialog." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "PanBar 无法推送预警通知。请到 系统设置 → 通知 → PanBar 中重新开启。无法推送时会用对话框兜底。" } } + "onboarding.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Welcome to PanBar" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "欢迎使用 PanBar" + } + } } }, - "alerts.permission.notDetermined" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Notification permission not granted yet." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "尚未授权 PanBar 发送通知。" } } + "onboarding.welcome.subtitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A lightweight menu bar app that scrolls your watchlist & holdings P&L in real-time. Three quick steps to set up." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "一个常驻菜单栏的轻量盯盘工具,实时滚动自选股价格与持仓盈亏。三步搞定。" + } + } } }, - "alerts.permission.request" : { + "onboarding.welcome.title" : { + "extractionState" : "stale", "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Request" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "请求授权" } } + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PanBar — live quotes in your menu bar" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "PanBar — 菜单栏上的实时盯盘" + } + } } }, - "alerts.primary" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Primary Condition" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "主条件" } } + "P" : { + + }, + "PanBar" : { + + }, + "popover.subtitle.holdings" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d holdings" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d 只持仓" + } + } } }, - "alerts.secondary" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Secondary Condition" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "副条件" } } + "popover.subtitle.markets" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d markets" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d 个市场" + } + } } }, - "alerts.secondary.enable" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Enable" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用" } } + "proxy.mode.manual" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Custom HTTP proxy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自定义 HTTP 代理" + } + } } }, - "alerts.secondary.disabled" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Enable to combine two conditions with AND / OR." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用后可以再加一条,与主条件用 AND / OR 组合。" } } + "proxy.mode.off" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Off (direct connection)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭(直连)" + } + } } }, - "alerts.logic" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Combine" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "组合方式" } } + "proxy.mode.system" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Follow system (default)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "跟随系统(默认)" + } + } } }, - "alert.logic.and" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "AND" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "同时满足 AND" } } + "scheme.east" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "East (red up, green down)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "东方 红涨绿跌" + } + } } }, - "alert.logic.or" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "OR" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "任一满足 OR" } } + "scheme.east.short" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "East" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "东方" + } + } + } + }, + "scheme.mono" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Monochrome" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "黑白" + } + } + } + }, + "scheme.mono.short" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mono" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "黑白" + } + } + } + }, + "scheme.west" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "West (green up, red down)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "西方 绿涨红跌" + } + } } }, - "alerts.advanced" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Advanced" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "高级" } } + "scheme.west.short" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "West" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "西方" + } + } } }, - "alerts.advanced.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Daily cap resets at midnight (CN time). Trading-hours-only checks if any of A/HK/US market is open." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每日次数上限按北京时间凌晨重置。仅交易时段=任一 A/港/美 市场开盘即可。" } } + "search.noResults" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No matches" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无匹配结果" + } + } } }, - "alerts.dailyCap" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Max triggers per day" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每日最多触发次数" } } + "search.placeholder" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Search: code / name / pinyin (e.g. 600519, AAPL, mtgz)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索:代码 / 名称 / 拼音(如 600519、AAPL、mtgz)" + } + } } }, - "alerts.tradingHoursOnly" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Trading hours only" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "仅交易时段" } } + "settings.about" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "About" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关于" + } + } } }, - "alerts.weekdaysOnly" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Weekdays only" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "仅工作日" } } + "settings.alerts" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerts" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预警" + } + } } }, - "alerts.col.todayCount" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Today" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "今日已触发" } } + "settings.backup.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exports everything (holdings / watchlist / alerts / settings / hotkeys / data sources / Finnhub key) to a single JSON file. Importing REPLACES all current data after confirmation." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出包含所有持仓 / 自选 / 预警 / 设置(语言 / 主题 / 配色 / 快捷键 / 数据源优先级 / Finnhub Key 等)为一个 JSON 文件。导入会先二次确认,然后整体覆盖当前数据。" + } + } } }, - "action.importCSV" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Import CSV" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导入 CSV" } } + "settings.backupSection" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Backup & Restore" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "备份与恢复" + } + } } }, - "action.exportCSV" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Export CSV" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出 CSV" } } + "settings.baseCurrency" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Base Currency" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "本位币" + } + } } }, - "csv.exportTitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Export to CSV" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出为 CSV" } } + "settings.browser" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open in" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "双击打开" + } + } } }, - "csv.importTitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Import from CSV" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从 CSV 导入" } } + "settings.colorScheme" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Color Scheme" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "涨跌配色" + } + } } }, "settings.dataSources" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Data Sources" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "数据源" } } + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data Sources" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据源" + } + } } }, - "settings.fx" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "FX Rates" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "汇率" } } + "settings.density" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Popover Density" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "面板密度" + } + } } }, - "fx.empty" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "No rates yet. Tap Refresh to load." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "还没有汇率数据,点「立即刷新」加载。" } } + "settings.fx" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "FX Rates" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "汇率" + } + } } }, - "fx.refreshNow" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Refresh now" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "立即刷新" } } + "settings.general" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "General" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通用" + } + } } }, - "fx.refresh.success" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Updated %d rates" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已更新 %d 对汇率" } } + "settings.hideOnScreenShare" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto-hide ticker during screen sharing" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "屏幕共享时自动隐藏滚动" + } + } } }, - "fx.refresh.failed" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Refresh failed — check network or data source" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新失败 — 检查网络或数据源" } } + "settings.hideOnScreenShare.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "macOS has no public API to detect ongoing screen recording. We scan for known sharing indicator windows (Zoom toolbar, Teams floating bar, etc.) — best effort, may miss some. For reliable hide use ⌘⌃M." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "macOS 没有公开 API 让 app 检测「正在录屏」。这里通过扫描 Zoom / Teams 等共享时才会出现的浮动窗口来识别,best-effort 可能漏报。需要 100% 可靠时用 ⌘⌃M 手动切换。" + } + } } }, - "fx.neverFetched" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Never fetched" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "尚未拉取" } } + "settings.hotkeys.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click a field, then press the new shortcut. Press ⌫ to clear, ⎋ to cancel." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点击下面任一框,按下新组合即可录入。⌫ 清除,⎋ 取消。" + } + } } }, - "fx.lastFetched" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Last fetched %@" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "上次拉取于 %@" } } + "settings.hotkeysSection" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hotkeys" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全局快捷键" + } + } } }, - "fx.autoRefresh" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Auto-refresh" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自动刷新" } } + "settings.language" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语言" + } + } } }, - "fx.autoRefresh.interval" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Interval" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新间隔" } } + "settings.launchAtLogin" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Launch at login" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "开机启动" + } + } } }, - "fx.autoRefresh.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Rates only change a few cents per day for most currencies — 1h is plenty. Set to Off if you'd rather refresh manually." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "汇率变化通常很小,1 小时一次足够。也可以关掉改为手动刷新。" } } + "settings.markets" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Markets" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "市场" + } + } } }, - "fx.interval.off" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Off" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭" } } + "settings.maxItems" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Max items in ticker: %d" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "滚动条最多展示数: %d" + } + } } }, - "fx.interval.5min" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 5 minutes" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 5 分钟" } } + "settings.pauseOnHover" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pause when hovering over the menu bar" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠标悬停菜单栏时暂停" + } + } } }, - "fx.interval.15min" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 15 minutes" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 15 分钟" } } + "settings.pauseWhenClosed" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pause when all markets closed" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全部市场休市时暂停" + } + } } }, - "fx.interval.1h" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every hour" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 1 小时" } } + "settings.portfolio" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Portfolio" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "持仓" + } + } } }, - "fx.interval.6h" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 6 hours" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 6 小时" } } + "settings.privacySection" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐私" + } + } } }, - "fx.interval.1d" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every day" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每天" } } + "settings.proxy.hint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Applies to all data sources (Tencent / EastMoney / Yahoo / Finnhub) and update checks." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "对全部数据源(腾讯 / 东方财富 / Yahoo / Finnhub)和检查更新都生效。" + } + } } }, - "fx.source" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Data Source" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "数据源" } } + "settings.proxyApply" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apply" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "应用" + } + } } }, - "fx.source.eastmoney" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "EastMoney (USDCNY / HKDCNY). Other directions are derived via CNY." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "东方财富(USDCNY / HKDCNY)。其它方向以 CNY 为枢轴换算。" } } + "settings.proxyHost" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Host" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "主机" + } + } } }, - "fx.time.seconds" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "%ds ago" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%d 秒前" } } + "settings.proxyMode" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proxy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "代理" + } + } } }, - "fx.time.minutes" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "%dm ago" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%d 分钟前" } } + "settings.proxyPort" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Port" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "端口" + } + } } }, - "fx.time.hours" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "%dh ago" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%d 小时前" } } + "settings.proxySection" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Network proxy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "网络代理" + } + } } }, - "fx.time.days" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "%dd ago" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%d 天前" } } + "settings.scrollSpeed" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scroll Speed" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "滚动速度" + } + } } }, "settings.theme" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Theme" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "主题" } } + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Theme" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "主题" + } + } } }, - "settings.density" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Popover Density" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "面板密度" } } + "settings.ticker" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu Bar" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "菜单栏" + } + } } }, - "settings.browser" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Open in" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "双击打开" } } + "settings.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PanBar Settings" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "PanBar 设置" + } + } } }, - "theme.system" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "System" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "跟随系统" } } + "settings.watchlist" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Watchlist" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自选" + } + } } }, - "theme.light" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Light" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "浅色" } } + "speed.fast" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fast" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快" + } + } } }, - "theme.dark" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Dark" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "深色" } } + "speed.medium" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medium" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "中" + } + } } }, - "density.compact" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Compact" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "紧凑" } } + "speed.slow" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Slow" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "慢" + } + } } }, - "density.standard" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Standard" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "标准" } } + "summary.allTime" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "All-Time" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "累计" + } + } } }, - "density.spacious" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Spacious" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "宽松" } } + "summary.positions" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d positions" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d 只持仓" + } + } } }, - "action.openInBrowser" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Open in browser" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在浏览器中打开" } } + "summary.today" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Today" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日" + } + } } }, - "dataSource.usOnly" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "US only" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "仅美股" } } + "summary.totalAssets" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total Assets" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "总资产" + } + } } }, - "dataSource.finnhub.help" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Free tier: 60 req/min for US stocks. Get an API key at finnhub.io." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "免费档 60 次/分钟,仅美股。前往 finnhub.io 注册获取 Key。" } } + "tab.alerts" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerts" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预警" + } + } } }, - "dataSource.finnhub.keyPlaceholder" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Paste your Finnhub API key" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "粘贴 Finnhub API Key" } } + "tab.holdings" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Holdings" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "持仓" + } + } } }, - "dataSource.finnhub.signup" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Get key →" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "去注册 →" } } + "tab.indices" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indices" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大盘" + } + } } }, - "onboarding.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Welcome to PanBar" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "欢迎使用 PanBar" } } + "tab.watchlist" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Watchlist" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自选" + } + } } }, - "onboarding.skip" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Skip" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "跳过" } } + "theme.dark" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dark" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "深色" + } + } } }, - "onboarding.back" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Back" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "上一步" } } + "theme.light" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Light" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "浅色" + } + } } }, - "onboarding.next" : { + "theme.system" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "System" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "跟随系统" + } + } + } + }, + "ticker.autoMenuBarWidth" : { + "extractionState" : "manual", "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Next" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "下一步" } } + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatic menu bar width" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动菜单栏宽度" + } + } } }, - "onboarding.finish" : { + "ticker.carouselDwell" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carousel interval" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "轮播间隔" + } + } + } + }, + "ticker.contentSection" : { + "extractionState" : "manual", "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Finish" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "完成" } } + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display Content" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示内容" + } + } } }, - "onboarding.welcome.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "PanBar — live quotes in your menu bar" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "PanBar — 菜单栏上的实时盯盘" } } + "ticker.displayMode" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display mode" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "展现形式" + } + } } }, - "onboarding.welcome.subtitle" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "A lightweight menu bar app that scrolls your watchlist & holdings P&L in real-time. Three quick steps to set up." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "一个常驻菜单栏的轻量盯盘工具,实时滚动自选股价格与持仓盈亏。三步搞定。" } } + "ticker.dwell.2s" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 2 seconds (fast)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 2 秒(快)" + } + } } }, - "onboarding.add.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Add your first stock" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加你的第一只股票" } } + "ticker.dwell.3s" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 3 seconds" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 3 秒" + } + } } }, - "onboarding.add.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Pick a market and enter a code. You can add more later in Settings." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "选择市场、填入代码即可。之后可在设置里继续添加。" } } + "ticker.dwell.4s" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 4 seconds (default)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 4 秒(默认)" + } + } } }, - "onboarding.add.toWatchlist" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Add to Watchlist" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加到自选" } } + "ticker.dwell.6s" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 6 seconds" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 6 秒" + } + } } }, - "onboarding.add.added" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Added!" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已添加" } } + "ticker.dwell.10s" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every 10 seconds (relaxed)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每 10 秒(慢)" + } + } } }, - "onboarding.color.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Pick a color scheme" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "选个涨跌配色" } } + "ticker.empty" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loading…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载中…" + } + } } }, - "onboarding.color.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "East: red up / green down. West: green up / red down. Mono: high-contrast." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "东方红涨绿跌,西方绿涨红跌,黑白则不分颜色。" } } + "ticker.holdingsSection" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Holdings" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "持仓" + } + } } }, - "scheme.east.short" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "East" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "东方" } } + "ticker.indicesHint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pick which indices appear in the menu bar scroller. Refreshed every 15s." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "勾选哪些大盘指数显示在菜单栏滚动条。15 秒刷新一次。" + } + } } }, - "scheme.west.short" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "West" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "西方" } } + "ticker.indicesSection" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Market Indices" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大盘指数" + } + } } }, - "scheme.mono.short" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Mono" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "黑白" } } + "ticker.itemsHint.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Each holding and watchlist row has a \"Ticker\" switch — go to Settings → Portfolio / Watchlist to enable or disable individual items." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "持仓和自选每一行都有\"滚动\"开关 — 到 设置 → 持仓 / 自选 单独开关每一条。" + } + } } }, - "menu.checkForUpdates" : { + "ticker.itemsHint.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pick stocks per row" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "单只勾选" + } + } + } + }, + "ticker.menuBarWidth" : { + "extractionState" : "manual", "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Check for Updates" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "检查更新" } } + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu bar width: %d pt" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "菜单栏宽度: %d pt" + } + } } }, - "update.checking" : { + "ticker.minimalMetric" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show metric" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示指标" + } + } + } + }, + "ticker.modeSettingsSection" : { + "extractionState" : "manual", "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Checking…" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "检查中…" } } + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display Mode Settings" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "展现形式设置" + } + } } }, - "ticker.carouselDwell" : { + "ticker.showAllTimePnL" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show all-time P&L" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示累计盈亏" + } + } + } + }, + "ticker.showAppIcon" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show app icon" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示软件图标" + } + } + } + }, + "ticker.showDirectionArrow" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show direction arrow" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示方向箭头" + } + } + } + }, + "ticker.showQuoteCode" : { + "extractionState" : "manual", "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Carousel interval" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "轮播间隔" } } + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show stock code" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示股票代码" + } + } + } + }, + "ticker.showQuoteName" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show stock name" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示股票名字" + } + } } }, - "ticker.dwell.2s" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 2 seconds (fast)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 2 秒(快)" } } + "ticker.showTodayPnL" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show today's P&L" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示今日盈亏" + } + } } }, - "ticker.dwell.3s" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 3 seconds" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 3 秒" } } + "ticker.showTotalAssets" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show total assets" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示当前市值" + } + } } }, - "ticker.dwell.4s" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 4 seconds (default)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 4 秒(默认)" } } + "ticker.summaryHint" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Applies to all display modes. Fixed mode shows only these summary metrics." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "对全部展现形式生效。固定模式只显示这些汇总指标,不展示个股。" + } + } } }, - "ticker.dwell.6s" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 6 seconds" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 6 秒" } } + "ticker.summarySection" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Summary metrics" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "汇总指标" + } + } } }, - "ticker.dwell.10s" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Every 10 seconds (relaxed)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每 10 秒(慢)" } } + "ticker.watchlistSection" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Watchlist" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自选" + } + } } }, - "settings.proxySection" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Network proxy" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "网络代理" } } + "update.action.download" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "去下载" + } + } } }, - "settings.proxyMode" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Proxy" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "代理" } } + "update.action.later" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Later" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "稍后" + } + } } }, - "settings.proxyHost" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Host" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "主机" } } + "update.available.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Current: %@\nLatest: %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前版本:%@\n最新版本:%@" + } + } } }, - "settings.proxyPort" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Port" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "端口" } } + "update.available.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update available — %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "有新版本可用 — %@" + } + } } }, - "settings.proxyApply" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Apply" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "应用" } } + "update.checking" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检查中…" + } + } } }, - "settings.proxy.hint" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Applies to all data sources (Tencent / EastMoney / Yahoo / Finnhub) and update checks." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "对全部数据源(腾讯 / 东方财富 / Yahoo / Finnhub)和检查更新都生效。" } } + "update.error.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Could not reach GitHub. Check your network and try again." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法连接 GitHub,请检查网络后重试。" + } + } } }, - "proxy.mode.off" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Off (direct connection)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭(直连)" } } + "update.error.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update check failed" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检查更新失败" + } + } } }, - "proxy.mode.system" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Follow system (default)" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "跟随系统(默认)" } } + "update.uptodate.body" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You're on PanBar %@, the latest available." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前 PanBar %@ 已经是最新版。" + } + } } }, - "proxy.mode.manual" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Custom HTTP proxy" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自定义 HTTP 代理" } } + "update.uptodate.title" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Already up to date" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已是最新版本" + } + } } }, - "action.dragToReorder" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Drag to reorder" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "拖动改顺序" } } + "watchlist.addTitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add to Watchlist" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加自选" + } + } } }, - "delete.confirm.title" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Delete %@?" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "确定删除「%@」?" } } + "watchlist.editTitle" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit Watchlist Item" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑自选" + } + } } }, - "delete.confirm.body" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "This action cannot be undone." } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "此操作无法撤销。" } } + "watchlist.empty" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No watchlist items." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "还没有自选股" + } + } } }, - "about.reportIssue" : { - "localizations" : { - "en" : { "stringUnit" : { "state" : "translated", "value" : "Report an Issue" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "反馈问题" } } + "watchlist.quickAdd" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add to Watchlist" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加自选" + } + } } } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/PanBar/Settings/Panes/PortfolioPane.swift b/PanBar/Settings/Panes/PortfolioPane.swift index 5c1a59a..28c3498 100644 --- a/PanBar/Settings/Panes/PortfolioPane.swift +++ b/PanBar/Settings/Panes/PortfolioPane.swift @@ -1,5 +1,46 @@ import SwiftUI +private struct PortfolioColumnWidths { + let horizontalPadding: CGFloat = 10 + let spacing: CGFloat = 8 + let drag: CGFloat = 18 + let symbol: CGFloat = 62 + let market: CGFloat = 34 + let actions: CGFloat = 44 + let name: CGFloat + let quantity: CGFloat + let cost: CGFloat + let contentWidth: CGFloat + + init(totalWidth: CGFloat, holdings: [Holding], nameHeader: String) { + let quantityBase: CGFloat = 48 + let costBase: CGFloat = 76 + name = ([nameHeader] + holdings.map(\.name)) + .map(Self.measuredNameWidth) + .max() ?? Self.measuredNameWidth(nameHeader) + + let fixedWidth = horizontalPadding * 2 + + spacing * 6 + + drag + + symbol + + name + + market + + actions + let flexibleBase = quantityBase + costBase + let extraWidth = max(0, totalWidth - fixedWidth - flexibleBase) + + quantity = quantityBase + extraWidth / 2 + cost = costBase + extraWidth / 2 + contentWidth = fixedWidth + flexibleBase + extraWidth + } + + private static func measuredNameWidth(_ value: String) -> CGFloat { + let font = NSFont.systemFont(ofSize: 13) + let width = (value as NSString).size(withAttributes: [.font: font]).width + return ceil(width) + 8 + } +} + struct PortfolioPane: View { @Environment(\.container) private var container @State private var holdings: [Holding] = [] @@ -93,64 +134,82 @@ struct PortfolioPane: View { /// 拖拽:.draggable(行 ID)+ .dropDestination(整行作为放置目标)。 /// 选中:单击置 selectedID,改背景色。 private var holdingsList: some View { - VStack(alignment: .leading, spacing: 0) { - HStack(spacing: 8) { - Text("").frame(width: 18) - Text(L("col.symbol", comment: "")).frame(width: 70, alignment: .leading) - Text(L("col.name", comment: "")).frame(minWidth: 100, maxWidth: .infinity, alignment: .leading) - Text(L("col.market", comment: "")).frame(width: 56, alignment: .leading) - Text(L("col.qty", comment: "")).frame(width: 70, alignment: .trailing) - Text(L("col.cost", comment: "")).frame(width: 100, alignment: .trailing) - Spacer().frame(width: 56) - } - .font(.system(size: 11, weight: .semibold)) - .foregroundColor(.secondary) - .padding(.horizontal, 10) - .padding(.vertical, 6) - .background(Color.secondary.opacity(0.08)) + GeometryReader { proxy in + let nameHeader = L("col.name", comment: "") + let columns = PortfolioColumnWidths(totalWidth: proxy.size.width, holdings: holdings, nameHeader: nameHeader) - ScrollView { - LazyVStack(spacing: 0) { - ForEach(Array(holdings.enumerated()), id: \.element.id) { index, h in - holdingRow(h, index: index, isAlternate: index.isMultiple(of: 2)) + ScrollView(.horizontal) { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: columns.spacing) { + Text("").frame(width: columns.drag) + Text(L("col.symbol", comment: "")).frame(width: columns.symbol, alignment: .leading) + Text(nameHeader).frame(width: columns.name, alignment: .leading) + Text(L("col.market", comment: "")).frame(width: columns.market, alignment: .leading) + Text(L("col.qty", comment: "")).frame(width: columns.quantity, alignment: .center) + Text(L("col.cost", comment: "")).frame(width: columns.cost, alignment: .center) + Text("").frame(width: columns.actions) } + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(.secondary) + .padding(.horizontal, columns.horizontalPadding) + .padding(.vertical, 6) + .background(Color.secondary.opacity(0.08)) + + ScrollView { + LazyVStack(spacing: 0) { + ForEach(Array(holdings.enumerated()), id: \.element.id) { index, h in + holdingRow(h, index: index, isAlternate: index.isMultiple(of: 2), columns: columns) + } + } + } + .background(Color(NSColor.controlBackgroundColor)) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color.secondary.opacity(0.2), lineWidth: 1) + ) } + .frame(width: columns.contentWidth, height: proxy.size.height, alignment: .topLeading) } - .background(Color(NSColor.controlBackgroundColor)) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color.secondary.opacity(0.2), lineWidth: 1) - ) + .frame(width: proxy.size.width, height: proxy.size.height, alignment: .topLeading) + .clipped() } + .frame(minHeight: 260) } @ViewBuilder - private func holdingRow(_ h: Holding, index: Int, isAlternate: Bool) -> some View { + private func holdingRow(_ h: Holding, index: Int, isAlternate: Bool, columns: PortfolioColumnWidths) -> some View { let isSelected = selectedID == h.id let isDropTarget = dropTargetID == h.id - HStack(spacing: 8) { + HStack(spacing: columns.spacing) { Image(systemName: "line.3.horizontal") .font(.system(size: 11)) .foregroundColor(.secondary.opacity(0.55)) - .frame(width: 18) + .frame(width: columns.drag) .help(L("action.dragToReorder", comment: "")) Text(h.symbol.market == .us ? h.symbol.code.uppercased() : h.symbol.code) .monospacedDigit() - .frame(width: 70, alignment: .leading) + .lineLimit(1) + .minimumScaleFactor(0.85) + .frame(width: columns.symbol, alignment: .leading) Text(h.name) .lineLimit(1) .truncationMode(.tail) - .frame(minWidth: 100, maxWidth: .infinity, alignment: .leading) + .frame(width: columns.name, alignment: .leading) Text(h.symbol.market.displayName) - .frame(width: 56, alignment: .leading) + .lineLimit(1) + .frame(width: columns.market, alignment: .leading) .foregroundColor(.secondary) - Text("\(h.quantity)") + Text(NSDecimalNumber(decimal: h.quantity).stringValue) .monospacedDigit() - .frame(width: 70, alignment: .trailing) - Text(h.currency.format(h.costPrice)) + .lineLimit(1) + .minimumScaleFactor(0.8) + .frame(width: columns.quantity, alignment: .center) + Text(h.currency.format(h.costPrice, fractionDigits: 3)) .monospacedDigit() - .frame(width: 100, alignment: .trailing) + .lineLimit(1) + .minimumScaleFactor(0.8) + .frame(width: columns.cost, alignment: .center) HStack(spacing: 4) { Button { editing = h } label: { Image(systemName: "pencil") @@ -164,9 +223,9 @@ struct PortfolioPane: View { } .buttonStyle(.borderless) } - .frame(width: 56) + .frame(width: columns.actions) } - .padding(.horizontal, 10) + .padding(.horizontal, columns.horizontalPadding) .padding(.vertical, 6) .background(rowBackground(isSelected: isSelected, isAlternate: isAlternate)) .overlay(alignment: .top) { diff --git a/PanBar/Settings/Panes/TickerPane.swift b/PanBar/Settings/Panes/TickerPane.swift index 8196298..34bdac5 100644 --- a/PanBar/Settings/Panes/TickerPane.swift +++ b/PanBar/Settings/Panes/TickerPane.swift @@ -30,16 +30,29 @@ private struct TickerPaneContent: View { Text(displayModeHint) .font(.caption) .foregroundColor(.secondary) + } - if prefs.displayMode == .scroll { + Section(header: Text(L("ticker.modeSettingsSection", comment: "")).font(.headline)) { + switch prefs.displayMode { + case .scroll, .scrollNoCode: Picker(L("settings.scrollSpeed", comment: ""), selection: $prefs.scrollSpeed) { ForEach(ScrollSpeed.allCases) { s in Text(s.displayName).tag(s) } } Toggle(L("settings.pauseOnHover", comment: ""), isOn: $prefs.pauseOnHover) - } - if prefs.displayMode == .carousel { + Toggle(L("settings.pauseWhenClosed", comment: ""), isOn: $prefs.pauseWhenClosed) + Stepper(value: $prefs.maxItems, in: 1...50) { + Text(String(format: L("settings.maxItems", comment: ""), prefs.maxItems)) + } + Toggle(L("ticker.autoMenuBarWidth", comment: ""), isOn: $prefs.scrollAutoWidth) + if !prefs.scrollAutoWidth { + Stepper(value: $prefs.scrollMenuBarWidth, in: 160...720, step: 20) { + Text(String(format: L("ticker.menuBarWidth", comment: ""), prefs.scrollMenuBarWidth)) + } + } + + case .carousel: Toggle(L("settings.pauseOnHover", comment: ""), isOn: $prefs.pauseOnHover) Picker(L("ticker.carouselDwell", comment: ""), selection: $prefs.carouselDwell) { Text(L("ticker.dwell.2s", comment: "")).tag(2) @@ -48,85 +61,103 @@ private struct TickerPaneContent: View { Text(L("ticker.dwell.6s", comment: "")).tag(6) Text(L("ticker.dwell.10s", comment: "")).tag(10) } - } - if prefs.displayMode == .minimal { - Picker(L("ticker.minimalMetric", comment: ""), selection: $prefs.minimalMetric) { - ForEach(MinimalMetric.allCases) { m in - Text(m.displayName).tag(m) - } - } - } - Toggle(L("settings.pauseWhenClosed", comment: ""), isOn: $prefs.pauseWhenClosed) - if prefs.displayMode == .scroll || prefs.displayMode == .carousel { + Toggle(L("settings.pauseWhenClosed", comment: ""), isOn: $prefs.pauseWhenClosed) Stepper(value: $prefs.maxItems, in: 1...50) { Text(String(format: L("settings.maxItems", comment: ""), prefs.maxItems)) } + Toggle(L("ticker.autoMenuBarWidth", comment: ""), isOn: $prefs.carouselAutoWidth) + if !prefs.carouselAutoWidth { + Stepper(value: $prefs.carouselMenuBarWidth, in: 100...360, step: 20) { + Text(String(format: L("ticker.menuBarWidth", comment: ""), prefs.carouselMenuBarWidth)) + } + } + + case .compact, .minimal: + Toggle(L("ticker.autoMenuBarWidth", comment: ""), isOn: $prefs.compactAutoWidth) + if !prefs.compactAutoWidth { + Stepper(value: $prefs.compactMenuBarWidth, in: 60...360, step: 20) { + Text(String(format: L("ticker.menuBarWidth", comment: ""), prefs.compactMenuBarWidth)) + } + } + } + } + + Section(header: Text(L("ticker.contentSection", comment: "")).font(.headline)) { + Toggle(L("ticker.showAppIcon", comment: ""), isOn: $prefs.showAppIcon) + if showsQuoteContentControls { + Toggle(L("ticker.showQuoteCode", comment: ""), isOn: $prefs.showQuoteCode) + Toggle(L("ticker.showQuoteName", comment: ""), isOn: $prefs.showQuoteName) } } Section(header: Text(L("ticker.summarySection", comment: "")).font(.headline)) { Toggle(L("ticker.showTodayPnL", comment: ""), isOn: $prefs.showTodayPnL) - Toggle(L("ticker.showTotalAssets", comment: ""), isOn: $prefs.showTotalAssets) Toggle(L("ticker.showAllTimePnL", comment: ""), isOn: $prefs.showAllTimePnL) + Toggle(L("ticker.showTotalAssets", comment: ""), isOn: $prefs.showTotalAssets) + if prefs.displayMode == .compact || prefs.displayMode == .minimal { + Toggle(L("ticker.showDirectionArrow", comment: ""), isOn: $prefs.showDirectionArrow) + } Text(L("ticker.summaryHint", comment: "")) .font(.caption) .foregroundColor(.secondary) } - Section(header: Text(L("ticker.indicesSection", comment: "")).font(.headline)) { - ForEach(IndexCatalog.all) { desc in - Toggle(isOn: Binding( - get: { prefs.tickerIndexIDs.contains(desc.id) }, - set: { newValue in - var s = prefs.tickerIndexIDs - if newValue { s.insert(desc.id) } else { s.remove(desc.id) } - prefs.tickerIndexIDs = s - } - )) { - HStack { - Text(desc.displayName) - marketBadge(desc.market) + if showsQuoteContentControls { + Section(header: Text(L("ticker.indicesSection", comment: "")).font(.headline)) { + ForEach(IndexCatalog.all) { desc in + Toggle(isOn: Binding( + get: { prefs.tickerIndexIDs.contains(desc.id) }, + set: { newValue in + var s = prefs.tickerIndexIDs + if newValue { s.insert(desc.id) } else { s.remove(desc.id) } + prefs.tickerIndexIDs = s + } + )) { + HStack { + Text(desc.displayName) + marketBadge(desc.market) + } } } - } - Text(L("ticker.indicesHint", comment: "")) - .font(.caption) - .foregroundColor(.secondary) - } - - Section(header: Text(L("ticker.holdingsSection", comment: "")).font(.headline)) { - if holdings.isEmpty { - Text(L("holdings.empty", comment: "")) + Text(L("ticker.indicesHint", comment: "")) .font(.caption) .foregroundColor(.secondary) - } else { - ForEach(holdings) { h in - Toggle(isOn: bindingFor(holding: h)) { - HStack { - Text(h.symbol.market == .us ? h.symbol.code.uppercased() : h.symbol.code) - .monospacedDigit() - Text(h.name) - .foregroundColor(.secondary) - marketBadge(h.symbol.market) + } + + Section(header: Text(L("ticker.holdingsSection", comment: "")).font(.headline)) { + if holdings.isEmpty { + Text(L("holdings.empty", comment: "")) + .font(.caption) + .foregroundColor(.secondary) + } else { + ForEach(holdings) { h in + Toggle(isOn: bindingFor(holding: h)) { + HStack { + Text(h.symbol.market == .us ? h.symbol.code.uppercased() : h.symbol.code) + .monospacedDigit() + Text(h.name) + .foregroundColor(.secondary) + marketBadge(h.symbol.market) + } } } } } - } - Section(header: Text(L("ticker.watchlistSection", comment: "")).font(.headline)) { - if watchlist.isEmpty { - Text(L("watchlist.empty", comment: "")) - .font(.caption) - .foregroundColor(.secondary) - } else { - ForEach(watchlist) { w in - Toggle(isOn: bindingFor(watch: w)) { - HStack { - Text(w.symbol.market == .us ? w.symbol.code.uppercased() : w.symbol.code) - Text(w.name) - .foregroundColor(.secondary) - marketBadge(w.symbol.market) + Section(header: Text(L("ticker.watchlistSection", comment: "")).font(.headline)) { + if watchlist.isEmpty { + Text(L("watchlist.empty", comment: "")) + .font(.caption) + .foregroundColor(.secondary) + } else { + ForEach(watchlist) { w in + Toggle(isOn: bindingFor(watch: w)) { + HStack { + Text(w.symbol.market == .us ? w.symbol.code.uppercased() : w.symbol.code) + Text(w.name) + .foregroundColor(.secondary) + marketBadge(w.symbol.market) + } } } } @@ -141,12 +172,17 @@ private struct TickerPaneContent: View { private var displayModeHint: String { switch prefs.displayMode { case .scroll: return L("displayMode.scroll.hint", comment: "") + case .scrollNoCode: return L("displayMode.scroll.hint", comment: "") case .carousel: return L("displayMode.carousel.hint", comment: "") case .compact: return L("displayMode.compact.hint", comment: "") case .minimal: return L("displayMode.minimal.hint", comment: "") } } + private var showsQuoteContentControls: Bool { + prefs.displayMode == .scroll || prefs.displayMode == .scrollNoCode || prefs.displayMode == .carousel + } + private func marketBadge(_ m: Market) -> some View { Text(m.displayName) .font(.system(size: 10)) diff --git a/project.yml b/project.yml index ba7e9e9..e053235 100644 --- a/project.yml +++ b/project.yml @@ -1,6 +1,7 @@ name: PanBar options: bundleIdPrefix: app.panbar + xcodeVersion: "26.5" deploymentTarget: macOS: "13.0" createIntermediateGroups: true From 4a8b645db4202654338aa1e85656dda395322eac Mon Sep 17 00:00:00 2001 From: zhenan <1254455745@qq.com> Date: Sun, 31 May 2026 13:46:17 +0800 Subject: [PATCH 2/6] Address ticker width review feedback --- PanBar/Infrastructure/TickerPreferences.swift | 43 +++++++++++++++---- PanBar/MenuBar/CarouselTickerView.swift | 8 ++-- PanBar/MenuBar/CompactTickerView.swift | 6 ++- PanBar/MenuBar/TickerRenderer.swift | 1 + PanBar/MenuBar/TickerView.swift | 8 ++-- 5 files changed, 50 insertions(+), 16 deletions(-) diff --git a/PanBar/Infrastructure/TickerPreferences.swift b/PanBar/Infrastructure/TickerPreferences.swift index 0173046..d80eae8 100644 --- a/PanBar/Infrastructure/TickerPreferences.swift +++ b/PanBar/Infrastructure/TickerPreferences.swift @@ -43,7 +43,7 @@ final class TickerPreferences: ObservableObject { } @Published var menuBarWidth: Int { didSet { - let clamped = min(520, max(80, menuBarWidth)) + let clamped = Self.clampMenuBarWidth(menuBarWidth) if clamped != menuBarWidth { menuBarWidth = clamped } else { @@ -53,7 +53,7 @@ final class TickerPreferences: ObservableObject { } @Published var scrollMenuBarWidth: Int { didSet { - let clamped = min(720, max(160, scrollMenuBarWidth)) + let clamped = Self.clampScrollMenuBarWidth(scrollMenuBarWidth) if clamped != scrollMenuBarWidth { scrollMenuBarWidth = clamped } else { @@ -63,7 +63,7 @@ final class TickerPreferences: ObservableObject { } @Published var carouselMenuBarWidth: Int { didSet { - let clamped = min(360, max(100, carouselMenuBarWidth)) + let clamped = Self.clampCarouselMenuBarWidth(carouselMenuBarWidth) if clamped != carouselMenuBarWidth { carouselMenuBarWidth = clamped } else { @@ -73,7 +73,7 @@ final class TickerPreferences: ObservableObject { } @Published var compactMenuBarWidth: Int { didSet { - let clamped = min(360, max(60, compactMenuBarWidth)) + let clamped = Self.clampCompactMenuBarWidth(compactMenuBarWidth) if clamped != compactMenuBarWidth { compactMenuBarWidth = clamped } else { @@ -117,6 +117,22 @@ final class TickerPreferences: ObservableObject { private let repo: SettingsRepository + private static func clampMenuBarWidth(_ value: Int) -> Int { + min(520, max(80, value)) + } + + private static func clampScrollMenuBarWidth(_ value: Int) -> Int { + min(720, max(160, value)) + } + + private static func clampCarouselMenuBarWidth(_ value: Int) -> Int { + min(360, max(100, value)) + } + + private static func clampCompactMenuBarWidth(_ value: Int) -> Int { + min(360, max(60, value)) + } + init(repo: SettingsRepository) { self.repo = repo self.colorScheme = repo.colorScheme @@ -164,11 +180,22 @@ final class TickerPreferences: ObservableObject { self.showQuoteCode = repo.string(SettingsRepository.Keys.tickerShowQuoteCode) != "0" } self.showQuoteName = repo.string(SettingsRepository.Keys.tickerShowQuoteName) != "0" - let legacyWidth = Int(repo.string(SettingsRepository.Keys.tickerMenuBarWidth) ?? "") ?? 280 + let rawLegacyWidth = Int(repo.string(SettingsRepository.Keys.tickerMenuBarWidth) ?? "") ?? 280 + let legacyWidth = Self.clampMenuBarWidth(rawLegacyWidth) self.menuBarWidth = legacyWidth - self.scrollMenuBarWidth = Int(repo.string(SettingsRepository.Keys.tickerScrollMenuBarWidth) ?? "") ?? max(160, legacyWidth) - self.carouselMenuBarWidth = Int(repo.string(SettingsRepository.Keys.tickerCarouselMenuBarWidth) ?? "") ?? min(360, max(160, legacyWidth)) - self.compactMenuBarWidth = Int(repo.string(SettingsRepository.Keys.tickerCompactMenuBarWidth) ?? "") ?? 160 + try? repo.set(SettingsRepository.Keys.tickerMenuBarWidth, "\(legacyWidth)") + let rawScrollWidth = Int(repo.string(SettingsRepository.Keys.tickerScrollMenuBarWidth) ?? "") ?? max(160, legacyWidth) + let rawCarouselWidth = Int(repo.string(SettingsRepository.Keys.tickerCarouselMenuBarWidth) ?? "") ?? min(360, max(160, legacyWidth)) + let rawCompactWidth = Int(repo.string(SettingsRepository.Keys.tickerCompactMenuBarWidth) ?? "") ?? 160 + let scrollWidth = Self.clampScrollMenuBarWidth(rawScrollWidth) + let carouselWidth = Self.clampCarouselMenuBarWidth(rawCarouselWidth) + let compactWidth = Self.clampCompactMenuBarWidth(rawCompactWidth) + self.scrollMenuBarWidth = scrollWidth + self.carouselMenuBarWidth = carouselWidth + self.compactMenuBarWidth = compactWidth + try? repo.set(SettingsRepository.Keys.tickerScrollMenuBarWidth, "\(scrollWidth)") + try? repo.set(SettingsRepository.Keys.tickerCarouselMenuBarWidth, "\(carouselWidth)") + try? repo.set(SettingsRepository.Keys.tickerCompactMenuBarWidth, "\(compactWidth)") self.scrollAutoWidth = repo.string(SettingsRepository.Keys.tickerScrollAutoWidth) == "1" self.carouselAutoWidth = repo.string(SettingsRepository.Keys.tickerCarouselAutoWidth) == "1" self.compactAutoWidth = repo.string(SettingsRepository.Keys.tickerCompactAutoWidth) != "0" diff --git a/PanBar/MenuBar/CarouselTickerView.swift b/PanBar/MenuBar/CarouselTickerView.swift index e9fd62e..9c6b61e 100644 --- a/PanBar/MenuBar/CarouselTickerView.swift +++ b/PanBar/MenuBar/CarouselTickerView.swift @@ -25,14 +25,16 @@ final class CarouselTickerView: NSView { var preferredTotalWidth: CGFloat? var visibleTextWidth: CGFloat = 200 var privacyHidden: Bool = false + /// 没有任何可见内容时仍保留一点点击区域,但不显示占位文字。 + private let emptyHitTargetWidth: CGFloat = 24 var totalWidth: CGFloat { - if items.isEmpty { - return leadingTextX + 4 - } if let preferredTotalWidth { return max(40, preferredTotalWidth) } + if items.isEmpty { + return max(emptyHitTargetWidth, leadingTextX + 4) + } return leadingTextX + currentTextWidth + 4 } diff --git a/PanBar/MenuBar/CompactTickerView.swift b/PanBar/MenuBar/CompactTickerView.swift index 1801dc6..d7913d8 100644 --- a/PanBar/MenuBar/CompactTickerView.swift +++ b/PanBar/MenuBar/CompactTickerView.swift @@ -24,6 +24,8 @@ final class CompactTickerView: NSView { private let slotSpacing: CGFloat = 10 private let labelFont = NSFont.systemFont(ofSize: 9, weight: .semibold) private let valueFont = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .semibold) + /// 没有任何可见内容时仍保留一点点击区域,但不显示占位文字。 + private let emptyHitTargetWidth: CGFloat = 24 /// 估算的最长行宽,根据当前 slots 算出来。 private var contentWidth: CGFloat { @@ -38,11 +40,11 @@ final class CompactTickerView: NSView { } var totalWidth: CGFloat { - let width = contentWidth - guard width > 0 else { return leadingTextX + 4 } if let preferredTotalWidth { return max(40, preferredTotalWidth) } + let width = contentWidth + guard width > 0 else { return max(emptyHitTargetWidth, leadingTextX + 4) } return leadingTextX + width + 4 } diff --git a/PanBar/MenuBar/TickerRenderer.swift b/PanBar/MenuBar/TickerRenderer.swift index 6dda47a..973bfc1 100644 --- a/PanBar/MenuBar/TickerRenderer.swift +++ b/PanBar/MenuBar/TickerRenderer.swift @@ -31,6 +31,7 @@ struct TickerRenderer { /// 多项之间用 " · " 分隔。 func render(items: [TickerItem]) -> NSAttributedString { guard !items.isEmpty else { + // 用户关闭全部展示项时保持视觉空白;具体 view 会保留最小点击宽度。 return NSAttributedString() } diff --git a/PanBar/MenuBar/TickerView.swift b/PanBar/MenuBar/TickerView.swift index 63098a1..d09ef81 100644 --- a/PanBar/MenuBar/TickerView.swift +++ b/PanBar/MenuBar/TickerView.swift @@ -25,6 +25,8 @@ final class TickerView: NSView { var visibleTextWidth: CGFloat = 280 /// 隐私模式:屏幕共享或用户手动开启时,只显示图标和 "•••",不绘制具体行情。 var privacyHidden: Bool = false + /// 没有任何可见内容时仍保留一点点击区域,但不显示占位文字。 + private let emptyHitTargetWidth: CGFloat = 24 /// 文字宽度(单条 attributed 的渲染宽度)。 private var attributedWidth: CGFloat = 0 @@ -40,12 +42,12 @@ final class TickerView: NSView { override var allowsVibrancy: Bool { false } var totalWidth: CGFloat { - if attributed.length == 0 { - return leadingTextX + 4 - } if let preferredTotalWidth { return max(40, preferredTotalWidth) } + if attributed.length == 0 { + return max(emptyHitTargetWidth, leadingTextX + 4) + } return leadingTextX + visibleTextWidth + 4 } From 2cef50251431621f1bf4dccbfe93d71ea5ba5476 Mon Sep 17 00:00:00 2001 From: zhenan <1254455745@qq.com> Date: Sun, 31 May 2026 14:08:04 +0800 Subject: [PATCH 3/6] Stabilize holdings row columns --- PanBar/Popover/Views/HoldingsTab.swift | 34 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/PanBar/Popover/Views/HoldingsTab.swift b/PanBar/Popover/Views/HoldingsTab.swift index fdcc2c9..2a74337 100644 --- a/PanBar/Popover/Views/HoldingsTab.swift +++ b/PanBar/Popover/Views/HoldingsTab.swift @@ -131,6 +131,8 @@ private struct HoldingRow: View { /// hover 时显示 inline 编辑铅笔(放在 name 后面,不挡涨跌) let showEditButton: Bool let onEdit: () -> Void + private let allTimeColumnWidth: CGFloat = 86 + private let priceColumnWidth: CGFloat = 116 /// 只要有 quote(无论 position 有没有),立即就能算出原币种的盈亏。 /// 本位币换算需要 FX,只能依赖 position。 @@ -151,25 +153,29 @@ private struct HoldingRow: View { } var body: some View { - HStack(alignment: .top, spacing: 8) { + HStack(alignment: .top, spacing: 10) { VStack(alignment: .leading, spacing: 3) { HStack(spacing: 6) { Text(displayCode(holding.symbol)) .font(.system(size: 13, weight: .semibold)) + .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) Text(holding.name) .font(.system(size: 11)) .foregroundColor(.secondary) .lineLimit(1) - if showEditButton { - Button(action: onEdit) { - Image(systemName: "pencil") - .font(.system(size: 10, weight: .semibold)) - .foregroundColor(.accentColor) - } - .buttonStyle(.plain) - .help(L("action.edit", comment: "")) - .transition(.opacity) + .truncationMode(.tail) + Button(action: onEdit) { + Image(systemName: "pencil") + .font(.system(size: 10, weight: .semibold)) + .foregroundColor(.accentColor) } + .buttonStyle(.plain) + .help(L("action.edit", comment: "")) + .opacity(showEditButton ? 1 : 0) + .disabled(!showEditButton) + .frame(width: 12, height: 12) + .accessibilityHidden(!showEditButton) } // 右侧 3 行时,在两条左侧文字之间塞 Spacer 把第二行推到底, // 跟右侧的第三行(≈ base)平齐。右侧 2 行时不撑,正常紧贴排。 @@ -180,11 +186,12 @@ private struct HoldingRow: View { .font(.system(size: 10)) .foregroundColor(.secondary.opacity(0.85)) .monospacedDigit() + .lineLimit(1) + .truncationMode(.tail) } .frame(maxHeight: hasBaseConversion ? .infinity : nil, alignment: .top) .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) allTimeColumn - Spacer() VStack(alignment: .trailing, spacing: 3) { HStack(spacing: 4) { if let q = quote { @@ -208,6 +215,7 @@ private struct HoldingRow: View { ) } } + .frame(width: priceColumnWidth, alignment: .trailing) .layoutPriority(2) } .padding(.horizontal, density.rowHorizontalPadding) @@ -220,6 +228,7 @@ private struct HoldingRow: View { Text(L("summary.allTime", comment: "")) .font(.system(size: 10, weight: .medium)) .foregroundColor(.secondary) + .lineLimit(1) if let pnl = nativePnL { Text(signedPnL(pnl, currency: holding.currency)) .font(.system(size: 11, weight: .semibold)) @@ -232,6 +241,7 @@ private struct HoldingRow: View { .font(.system(size: 11, weight: .semibold)) .foregroundColor(.secondary) .monospacedDigit() + .lineLimit(1) } // 本位币换算依赖 FX,只能从 snapshot 拿。 if holding.currency != baseCurrency, @@ -244,7 +254,7 @@ private struct HoldingRow: View { .minimumScaleFactor(0.8) } } - .frame(width: 74, alignment: .leading) + .frame(width: allTimeColumnWidth, alignment: .leading) .layoutPriority(1) } From e6480828c2978b4baa1e45c4a4e7399ae9939870 Mon Sep 17 00:00:00 2001 From: zhenan <1254455745@qq.com> Date: Sun, 31 May 2026 14:15:23 +0800 Subject: [PATCH 4/6] Tune holdings row spacing --- PanBar/Popover/Views/HoldingsTab.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/PanBar/Popover/Views/HoldingsTab.swift b/PanBar/Popover/Views/HoldingsTab.swift index 2a74337..9b96dab 100644 --- a/PanBar/Popover/Views/HoldingsTab.swift +++ b/PanBar/Popover/Views/HoldingsTab.swift @@ -131,8 +131,8 @@ private struct HoldingRow: View { /// hover 时显示 inline 编辑铅笔(放在 name 后面,不挡涨跌) let showEditButton: Bool let onEdit: () -> Void - private let allTimeColumnWidth: CGFloat = 86 - private let priceColumnWidth: CGFloat = 116 + private let allTimeColumnWidth: CGFloat = 84 + private let priceColumnWidth: CGFloat = 104 /// 只要有 quote(无论 position 有没有),立即就能算出原币种的盈亏。 /// 本位币换算需要 FX,只能依赖 position。 @@ -153,11 +153,11 @@ private struct HoldingRow: View { } var body: some View { - HStack(alignment: .top, spacing: 10) { + HStack(alignment: .top, spacing: 8) { VStack(alignment: .leading, spacing: 3) { - HStack(spacing: 6) { + HStack(spacing: 4) { Text(displayCode(holding.symbol)) - .font(.system(size: 13, weight: .semibold)) + .font(.system(size: 12, weight: .medium)) .lineLimit(1) .fixedSize(horizontal: true, vertical: false) Text(holding.name) @@ -174,7 +174,7 @@ private struct HoldingRow: View { .help(L("action.edit", comment: "")) .opacity(showEditButton ? 1 : 0) .disabled(!showEditButton) - .frame(width: 12, height: 12) + .frame(width: 10, height: 12) .accessibilityHidden(!showEditButton) } // 右侧 3 行时,在两条左侧文字之间塞 Spacer 把第二行推到底, @@ -206,6 +206,8 @@ private struct HoldingRow: View { .monospacedDigit() } } + .lineLimit(1) + .minimumScaleFactor(0.85) if let pnl = nativeTodayPnL { labeledPnL( label: L("summary.today", comment: ""), From 5dbd829b154dcf699bbd40c1847a5551ce1f5e17 Mon Sep 17 00:00:00 2001 From: zhenan <1254455745@qq.com> Date: Sun, 31 May 2026 14:27:41 +0800 Subject: [PATCH 5/6] Adjust holdings symbol typography --- PanBar/Popover/Views/HoldingsTab.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PanBar/Popover/Views/HoldingsTab.swift b/PanBar/Popover/Views/HoldingsTab.swift index 9b96dab..d9e6cd4 100644 --- a/PanBar/Popover/Views/HoldingsTab.swift +++ b/PanBar/Popover/Views/HoldingsTab.swift @@ -157,11 +157,11 @@ private struct HoldingRow: View { VStack(alignment: .leading, spacing: 3) { HStack(spacing: 4) { Text(displayCode(holding.symbol)) - .font(.system(size: 12, weight: .medium)) + .font(.system(size: 11)) .lineLimit(1) .fixedSize(horizontal: true, vertical: false) Text(holding.name) - .font(.system(size: 11)) + .font(.system(size: 12, weight: .semibold)) .foregroundColor(.secondary) .lineLimit(1) .truncationMode(.tail) From 8c275a3602b829842c46d1a0151bb891a096efa0 Mon Sep 17 00:00:00 2001 From: zhenan <1254455745@qq.com> Date: Sun, 31 May 2026 14:38:24 +0800 Subject: [PATCH 6/6] Remove popover close observer token --- PanBar/Popover/PopoverController.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/PanBar/Popover/PopoverController.swift b/PanBar/Popover/PopoverController.swift index 0381998..ead8074 100644 --- a/PanBar/Popover/PopoverController.swift +++ b/PanBar/Popover/PopoverController.swift @@ -9,6 +9,7 @@ final class PopoverController { /// 监听 popover 之外的点击,关 popover。`.transient` 行为对菜单栏 popover 有时漏 /// (尤其是点系统菜单栏 / 通知 / 其它 app 时),这里多加一层保险。 private var eventMonitor: Any? + private var didCloseObserver: NSObjectProtocol? var onClose: (() -> Void)? var isShown: Bool { popover.isShown } @@ -46,7 +47,7 @@ final class PopoverController { // popover 通过 .transient 行为自己关时,也要更新 refresher 状态 // (否则 pace 会一直停在 .popoverOpen,后台多耗一点请求) - NotificationCenter.default.addObserver( + didCloseObserver = NotificationCenter.default.addObserver( forName: NSPopover.didCloseNotification, object: popover, queue: .main @@ -59,7 +60,9 @@ final class PopoverController { deinit { if let m = eventMonitor { NSEvent.removeMonitor(m) } - NotificationCenter.default.removeObserver(self) + if let observer = didCloseObserver { + NotificationCenter.default.removeObserver(observer) + } } private func handlePopoverClosed() {