fix: 修复配置重置问题并优化菜单交互体验

🐛 问题修复:
- 修复删除配置文件后币种设置不恢复默认值的问题
- 解决 macOS 沙盒缓存导致的配置读取不一致
- 增强币种配置验证逻辑,确保异常情况下的正确重置

 功能改进:
- 优化菜单布局,移除冗余的币种子菜单
- 改进价格显示,添加当前选中币种的✓标记
- 增强 Option+点击复制功能,支持未加载价格的异步获取
- 移除通知权限请求,简化用户交互

🔧 开发工具:
- 新增 resetToDefaults() 方法用于配置重置
- 添加 Debug 模式下的"重置设置"菜单项
- 改进错误处理和调试日志输出
- 新增 fetchSinglePrice() 方法支持单个币种价格获取

📝 项目配置:
- 更新版本号至 v1.0.3
- 完善应用版权信息和显示名称
- 修正部署目标版本信息
This commit is contained in:
ZhangLei
2025-10-30 14:35:36 +08:00
parent 9e805248cb
commit f25fc1ab6c
3 changed files with 155 additions and 119 deletions

View File

@@ -19,7 +19,7 @@ class AppSettings: ObservableObject {
@Published var refreshInterval: RefreshInterval = .thirtySeconds
///
@Published var selectedSymbol: CryptoSymbol = .btc
// MARK: - Private Properties
private let defaults = UserDefaults.standard
@@ -35,7 +35,7 @@ class AppSettings: ObservableObject {
// MARK: - Configuration Methods
/// UserDefaults
/// 使30
/// 使30 + BTC
func loadSettings() {
#if DEBUG
print("🔧 [AppSettings] 开始加载配置...")
@@ -74,13 +74,25 @@ class AppSettings: ObservableObject {
}
#endif
//
if hasSymbolKey,
let savedSymbolRaw = savedSymbolRaw,
!savedSymbolRaw.isEmpty, //
let savedSymbol = CryptoSymbol(rawValue: savedSymbolRaw) {
selectedSymbol = savedSymbol
#if DEBUG
print("🔧 [AppSettings] ✅ 使用保存的币种: \(savedSymbol.displayName)")
#endif
//
if CryptoSymbol.allCases.contains(savedSymbol) {
selectedSymbol = savedSymbol
#if DEBUG
print("🔧 [AppSettings] ✅ 使用保存的币种: \(savedSymbol.displayName)")
#endif
} else {
//
selectedSymbol = .btc
#if DEBUG
print("🔧 [AppSettings] ⚠️ 保存的币种不在支持列表中,重置为默认值: \(selectedSymbol.displayName)")
#endif
saveSelectedSymbol(.btc)
}
} else {
selectedSymbol = .btc
#if DEBUG
@@ -94,6 +106,25 @@ class AppSettings: ObservableObject {
#endif
}
///
///
func resetToDefaults() {
#if DEBUG
print("🔧 [AppSettings] 重置所有设置为默认值")
#endif
refreshInterval = .thirtySeconds
selectedSymbol = .btc
//
saveRefreshInterval(.thirtySeconds)
saveSelectedSymbol(.btc)
#if DEBUG
print("🔧 [AppSettings] 重置完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName)")
#endif
}
///
/// - Parameter interval:
func saveRefreshInterval(_ interval: RefreshInterval) {

View File

@@ -8,7 +8,6 @@
import SwiftUI
import AppKit
import Combine
import UserNotifications
// macOS
@MainActor
@@ -85,7 +84,7 @@ class BTCMenuBarApp: NSObject, ObservableObject {
.store(in: &cancellables)
}
//
//
private func updateMenuBarTitle(price: Double) {
DispatchQueue.main.async {
guard let button = self.statusItem?.button else { return }
@@ -145,17 +144,22 @@ class BTCMenuBarApp: NSObject, ObservableObject {
private func showMenu(from view: NSView) {
let menu = NSMenu()
//
//
//
var symbolMenuItems: [CryptoSymbol: NSMenuItem] = [:]
let currentSymbol = priceManager.selectedSymbol
for symbol in CryptoSymbol.allCases {
let placeholderTitle = "\(symbol.displayName): 加载中..."
let item = NSMenuItem(title: placeholderTitle, action: nil, keyEquivalent: "")
let isCurrent = (symbol == currentSymbol)
let placeholderTitle = isCurrent ? "\(symbol.displayName): 加载中..." : " \(symbol.displayName): 加载中..."
let item = NSMenuItem(title: placeholderTitle, action: #selector(self.selectOrCopySymbol(_:)), keyEquivalent: "")
item.target = self // target
if let icon = symbolImage(for: symbol) {
icon.size = NSSize(width: 16, height: 16)
item.image = icon
}
item.isEnabled = false
item.isEnabled = true //
item.representedObject = ["symbol": symbol, "price": 0.0]
menu.addItem(item)
symbolMenuItems[symbol] = item
}
@@ -163,32 +167,36 @@ class BTCMenuBarApp: NSObject, ObservableObject {
//
Task { @MainActor in
let results = await self.priceManager.fetchAllPrices()
let currentSymbolAfter = self.priceManager.selectedSymbol
for symbol in CryptoSymbol.allCases {
guard let (priceOpt, errorOpt) = results[symbol], let menuItem = symbolMenuItems[symbol] else { continue }
let isCurrent = (symbol == currentSymbolAfter)
if let price = priceOpt {
menuItem.title = "\(symbol.displayName): $\(self.formatPriceWithCommas(price))"
let title = isCurrent ? "\(symbol.displayName): $\(self.formatPriceWithCommas(price))" : " \(symbol.displayName): $\(self.formatPriceWithCommas(price))"
menuItem.title = title
menuItem.isEnabled = true //
menuItem.action = #selector(self.copyPriceOrSelectSymbol(_:))
menuItem.target = self
menuItem.target = self // target
menuItem.representedObject = ["symbol": symbol, "price": price]
} else if let error = errorOpt {
menuItem.title = "\(symbol.displayName): 错误"
let title = isCurrent ? "\(symbol.displayName): 错误" : " \(symbol.displayName): 错误"
menuItem.title = title
menuItem.toolTip = error
menuItem.isEnabled = true // 使
menuItem.action = #selector(self.selectSymbol(_:))
menuItem.target = self
menuItem.representedObject = symbol.rawValue
menuItem.isEnabled = false //
menuItem.target = self // target
} else {
menuItem.title = "\(symbol.displayName): 加载中..."
//
let title = isCurrent ? "\(symbol.displayName): 加载中..." : " \(symbol.displayName): 加载中..."
menuItem.title = title
menuItem.target = self // target
//
}
}
}
// 使
let hintItem = NSMenuItem(title: "💡 点击选择币种Option+点击复制价", action: nil, keyEquivalent: "")
hintItem.isEnabled = false
menu.addItem(hintItem)
// let hintItem = NSMenuItem(title: "💡 Option+", action: nil, keyEquivalent: "")
// hintItem.isEnabled = false
// menu.addItem(hintItem)
menu.addItem(NSMenuItem.separator())
//
@@ -214,36 +222,7 @@ class BTCMenuBarApp: NSObject, ObservableObject {
menu.addItem(NSMenuItem.separator())
//
let symbolSettingsItem = NSMenuItem(title: "币种选择", action: nil, keyEquivalent: "")
if let symbolSettingsImage = NSImage(systemSymbolName: "chart.line.uptrend.xyaxis", accessibilityDescription: "币种选择") {
symbolSettingsImage.size = NSSize(width: 16, height: 16)
symbolSettingsItem.image = symbolSettingsImage
}
let symbolMenu = NSMenu()
let currentSymbol = priceManager.selectedSymbol
for symbol in CryptoSymbol.allCases {
let isCurrent = (symbol == currentSymbol)
let item = NSMenuItem(
title: symbol.menuTitle(isCurrent: isCurrent),
action: #selector(selectSymbol(_:)),
keyEquivalent: ""
)
item.target = self
item.isEnabled = !isCurrent
item.representedObject = symbol.rawValue
if let icon = symbolImage(for: symbol) {
icon.size = NSSize(width: 16, height: 16)
item.image = icon
}
symbolMenu.addItem(item)
}
symbolSettingsItem.submenu = symbolMenu
menu.addItem(symbolSettingsItem)
menu.addItem(NSMenuItem.separator())
//
let refreshTitle = priceManager.isFetching ? "刷新中..." : "刷新价格"
let refreshItem = NSMenuItem(title: refreshTitle, action: #selector(refreshPrice), keyEquivalent: "r")
@@ -285,6 +264,19 @@ class BTCMenuBarApp: NSObject, ObservableObject {
menu.addItem(NSMenuItem.separator())
#if DEBUG
// Debug
let resetItem = NSMenuItem(title: "重置设置", action: #selector(resetSettings), keyEquivalent: "")
if let resetImage = NSImage(systemSymbolName: "arrow.counterclockwise", accessibilityDescription: "重置设置") {
resetImage.size = NSSize(width: 16, height: 16)
resetItem.image = resetImage
}
resetItem.target = self
menu.addItem(resetItem)
menu.addItem(NSMenuItem.separator())
#endif
// GitHubGitHub
let checkUpdateItem = NSMenuItem(title: "GitHub", action: #selector(checkForUpdates), keyEquivalent: "")
if let updateImage = NSImage(systemSymbolName: "star.circle", accessibilityDescription: "GitHub") {
@@ -338,22 +330,12 @@ class BTCMenuBarApp: NSObject, ObservableObject {
}
}
//
@objc private func selectSymbol(_ sender: NSMenuItem) {
guard let rawValue = sender.representedObject as? String,
let symbol = CryptoSymbol(rawValue: rawValue) else {
return
}
appSettings.saveSelectedSymbol(symbol)
print("✅ 币种已更新为: \(symbol.pairDisplayName)")
}
//
@objc private func copyPriceOrSelectSymbol(_ sender: NSMenuItem) {
// Option
@objc private func selectOrCopySymbol(_ sender: NSMenuItem) {
guard let data = sender.representedObject as? [String: Any],
let symbol = data["symbol"] as? CryptoSymbol,
let price = data["price"] as? Double else {
let symbol = data["symbol"] as? CryptoSymbol else {
print("❌ 无法获取菜单项数据")
return
}
@@ -363,21 +345,35 @@ class BTCMenuBarApp: NSObject, ObservableObject {
if isOptionPressed {
//
let priceString = formatPriceWithCommas(price)
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString("$\(priceString)", forType: .string)
print("✅ 已复制 \(symbol.displayName) 价格到剪贴板: $\(priceString)")
//
Task {
await self.showCopyNotification(symbol: symbol, price: priceString)
let price = data["price"] as? Double ?? 0.0
//
if price == 0.0 {
Task { @MainActor in
print("🔄 价格未加载,正在获取 \(symbol.displayName) 价格...")
if let newPrice = await self.priceManager.fetchSinglePrice(for: symbol) {
let priceString = self.formatPriceWithCommas(newPrice)
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString("$\(priceString)", forType: .string)
print("✅ 已复制 \(symbol.displayName) 价格到剪贴板: $\(priceString)")
} else {
print("❌ 无法获取 \(symbol.displayName) 价格")
}
}
} else {
let priceString = formatPriceWithCommas(price)
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString("$\(priceString)", forType: .string)
print("✅ 已复制 \(symbol.displayName) 价格到剪贴板: $\(priceString)")
}
} else {
//
//
appSettings.saveSelectedSymbol(symbol)
print("✅ 币种已更新为: \(symbol.pairDisplayName)")
print("✅ 币种已更新为: \(symbol.displayName)")
}
}
@@ -413,12 +409,44 @@ class BTCMenuBarApp: NSObject, ObservableObject {
• 支持手动刷新 (Cmd+R)
• 智能错误重试机制
• 优雅的SF Symbols图标
💡 TIPS
• 点击币种名称为切换主菜单栏显示
• Option + 鼠标左键复制价格
"""
alert.alertStyle = .informational
alert.addButton(withTitle: "确定")
alert.runModal()
}
// Debug
@objc private func resetSettings() {
#if DEBUG
let alert = NSAlert()
alert.messageText = "重置设置"
alert.informativeText = "确定要将所有设置重置为默认值吗?\n\n• 币种BTC\n• 刷新间隔30秒"
alert.alertStyle = .warning
alert.addButton(withTitle: "确定")
alert.addButton(withTitle: "取消")
let response = alert.runModal()
if response == .alertFirstButtonReturn {
//
appSettings.resetToDefaults()
//
let confirmAlert = NSAlert()
confirmAlert.messageText = "重置完成"
confirmAlert.informativeText = "所有设置已重置为默认值,应用将立即生效。"
confirmAlert.alertStyle = .informational
confirmAlert.addButton(withTitle: "确定")
confirmAlert.runModal()
print("🔧 [BTCMenuBarApp] 用户手动重置了所有设置")
}
#endif
}
// GitHub
@objc private func checkForUpdates() {
let githubURL = "https://github.com/jiayouzl/Bitcoin-Monitoring"
@@ -445,42 +473,7 @@ class BTCMenuBarApp: NSObject, ObservableObject {
return version
}
//
private func showCopyNotification(symbol: CryptoSymbol, price: String) async {
let center = UNUserNotificationCenter.current()
//
do {
let granted = try await center.requestAuthorization(options: [.alert, .sound])
if !granted {
return //
}
} catch {
print("❌ 通知权限请求失败: \(error)")
return
}
//
let content = UNMutableNotificationContent()
content.title = "价格已复制"
content.body = "\(symbol.displayName): $\(price)"
content.sound = .default
//
let request = UNNotificationRequest(
identifier: "price-copied-\(Date().timeIntervalSince1970)",
content: content,
trigger: nil //
)
//
do {
try await center.add(request)
} catch {
print("❌ 通知发送失败: \(error)")
}
}
// 退
@objc private func quitApp() {
NSApplication.shared.terminate(nil)

View File

@@ -15,7 +15,7 @@ class PriceManager: ObservableObject {
@Published var isFetching: Bool = false
@Published var lastError: PriceError?
@Published var selectedSymbol: CryptoSymbol
private let priceService = PriceService()
private var timer: Timer?
private var currentRefreshInterval: TimeInterval = RefreshInterval.thirtySeconds.rawValue //
@@ -265,4 +265,16 @@ class PriceManager: ObservableObject {
return results
}
/// Option+
/// - Parameter symbol:
/// - Returns: nil
func fetchSinglePrice(for symbol: CryptoSymbol) async -> Double? {
do {
return try await priceService.fetchPrice(for: symbol)
} catch {
print("❌ 获取 \(symbol.displayName) 价格失败: \(error.localizedDescription)")
return nil
}
}
}