mirror of
https://github.com/jiayouzl/Bitcoin-Monitoring.git
synced 2025-11-24 19:12:58 +08:00
feat: 添加Option+点击功能设置,支持复制价格和打开币安交易页面
This commit is contained in:
@@ -78,7 +78,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1620;
|
||||
LastUpgradeCheck = 2600;
|
||||
LastUpgradeCheck = 2610;
|
||||
TargetAttributes = {
|
||||
4E94106F2EB09F90003658CB = {
|
||||
CreatedOnToolsVersion = 16.2;
|
||||
@@ -131,6 +131,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
@@ -196,6 +197,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
@@ -273,7 +275,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
MARKETING_VERSION = 1.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mark.bitcoin-monitoring";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -306,7 +308,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
MARKETING_VERSION = 1.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mark.bitcoin-monitoring";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -450,91 +450,130 @@ class MenuBarManager: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
|
||||
// 选择币种或复制价格(支持Option键切换功能)
|
||||
// 选择币种或执行Option+点击功能
|
||||
@objc private func selectOrCopySymbol(_ sender: NSMenuItem) {
|
||||
guard let data = sender.representedObject as? [String: Any] else {
|
||||
print("❌ 无法获取菜单项数据")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否按住了 Option 键,如果是则复制价格到剪贴板
|
||||
// 检查是否按住了 Option 键
|
||||
let currentEvent = NSApp.currentEvent
|
||||
let isOptionPressed = currentEvent?.modifierFlags.contains(.option) ?? false
|
||||
let isCustom = data["isCustom"] as? Bool ?? false
|
||||
|
||||
if isOptionPressed {
|
||||
// 复制价格到剪贴板
|
||||
let price = data["price"] as? Double ?? 0.0
|
||||
let displayName: String
|
||||
// 获取币种信息
|
||||
let displayName: String
|
||||
let symbolForURL: String // 用于生成币安URL的币种符号
|
||||
|
||||
if isCustom {
|
||||
guard let customSymbol = data["customSymbol"] as? CustomCryptoSymbol else {
|
||||
print("❌ 无法获取自定义币种数据")
|
||||
return
|
||||
}
|
||||
displayName = customSymbol.displayName
|
||||
} else {
|
||||
guard let symbol = data["symbol"] as? CryptoSymbol else {
|
||||
print("❌ 无法获取默认币种数据")
|
||||
return
|
||||
}
|
||||
displayName = symbol.displayName
|
||||
if isCustom {
|
||||
guard let customSymbol = data["customSymbol"] as? CustomCryptoSymbol else {
|
||||
print("❌ 无法获取自定义币种数据")
|
||||
return
|
||||
}
|
||||
displayName = customSymbol.displayName
|
||||
symbolForURL = customSymbol.symbol // 自定义币种的符号(如BTC, ETH)
|
||||
} else {
|
||||
guard let symbol = data["symbol"] as? CryptoSymbol else {
|
||||
print("❌ 无法获取默认币种数据")
|
||||
return
|
||||
}
|
||||
displayName = symbol.displayName
|
||||
symbolForURL = symbol.displayName // 使用displayName获取币种基础符号(如BTC, ETH)
|
||||
}
|
||||
|
||||
// 如果价格还没加载完成,先获取价格再复制
|
||||
if price == 0.0 {
|
||||
Task { @MainActor in
|
||||
print("🔄 价格未加载,正在获取 \(displayName) 价格...")
|
||||
var newPrice: Double?
|
||||
if isOptionPressed {
|
||||
// 根据用户设置的Option+点击功能执行相应操作
|
||||
let optionAction = appSettings.optionClickAction
|
||||
|
||||
if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol {
|
||||
newPrice = await self.priceManager.fetchCustomSymbolPrice(forApiSymbol: customSymbol.apiSymbol)
|
||||
} else if let symbol = data["symbol"] as? CryptoSymbol {
|
||||
newPrice = await self.priceManager.fetchSinglePrice(for: symbol)
|
||||
}
|
||||
switch optionAction {
|
||||
case .copyPrice:
|
||||
// 复制价格到剪贴板
|
||||
copyPriceToClipboard(symbol: displayName, data: data, isCustom: isCustom)
|
||||
|
||||
if let priceToCopy = newPrice {
|
||||
let priceString = self.formatPriceWithCommas(priceToCopy)
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString("$\(priceString)", forType: .string)
|
||||
|
||||
print("✅ 已复制 \(displayName) 价格到剪贴板: $\(priceString)")
|
||||
} else {
|
||||
print("❌ 无法获取 \(displayName) 价格")
|
||||
}
|
||||
case .openSpotTrading:
|
||||
// 打开币安现货交易页面
|
||||
let spotSuccess = BinanceURLGenerator.openSpotTradingPage(for: symbolForURL)
|
||||
if spotSuccess {
|
||||
print("✅ 已打开 \(displayName) 币安现货交易页面")
|
||||
} else {
|
||||
print("❌ 打开 \(displayName) 币安现货交易页面失败")
|
||||
}
|
||||
} else {
|
||||
let priceString = formatPriceWithCommas(price)
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString("$\(priceString)", forType: .string)
|
||||
|
||||
print("✅ 已复制 \(displayName) 价格到剪贴板: $\(priceString)")
|
||||
case .openFuturesTrading:
|
||||
// 打开币安合约交易页面
|
||||
let futuresSuccess = BinanceURLGenerator.openFuturesTradingPage(for: symbolForURL)
|
||||
if futuresSuccess {
|
||||
print("✅ 已打开 \(displayName) 币安合约交易页面")
|
||||
} else {
|
||||
print("❌ 打开 \(displayName) 币安合约交易页面失败")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 选择该币种
|
||||
if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol {
|
||||
// 选择自定义币种 - 找到对应的索引并选择
|
||||
if let index = appSettings.customCryptoSymbols.firstIndex(of: customSymbol) {
|
||||
appSettings.selectCustomCryptoSymbol(at: index)
|
||||
print("✅ 已切换到自定义币种: \(customSymbol.displayName)")
|
||||
// 正常点击:选择该币种
|
||||
selectSymbol(data: data, isCustom: isCustom, displayName: displayName)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制价格到剪贴板的辅助方法
|
||||
private func copyPriceToClipboard(symbol: String, data: [String: Any], isCustom: Bool) {
|
||||
let price = data["price"] as? Double ?? 0.0
|
||||
|
||||
// 如果价格还没加载完成,先获取价格再复制
|
||||
if price == 0.0 {
|
||||
Task { @MainActor in
|
||||
print("🔄 价格未加载,正在获取 \(symbol) 价格...")
|
||||
var newPrice: Double?
|
||||
|
||||
if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol {
|
||||
newPrice = await self.priceManager.fetchCustomSymbolPrice(forApiSymbol: customSymbol.apiSymbol)
|
||||
} else if let symbol = data["symbol"] as? CryptoSymbol {
|
||||
newPrice = await self.priceManager.fetchSinglePrice(for: symbol)
|
||||
}
|
||||
|
||||
// 立即更新价格管理器和UI
|
||||
self.priceManager.updateCryptoSymbolSettings()
|
||||
// 使用0.0价格强制更新显示状态,确保图标和文字都正确更新
|
||||
self.updateMenuBarTitle(price: 0.0)
|
||||
} else if let symbol = data["symbol"] as? CryptoSymbol {
|
||||
// 选择默认币种
|
||||
appSettings.saveSelectedSymbol(symbol)
|
||||
print("✅ 已切换到默认币种: \(symbol.displayName)")
|
||||
if let priceToCopy = newPrice {
|
||||
let priceString = self.formatPriceWithCommas(priceToCopy)
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString("$\(priceString)", forType: .string)
|
||||
|
||||
// 立即更新价格管理器和UI
|
||||
self.priceManager.updateCryptoSymbolSettings()
|
||||
// 使用0.0价格强制更新显示状态,确保图标和文字都正确更新
|
||||
self.updateMenuBarTitle(price: 0.0)
|
||||
print("✅ 已复制 \(symbol) 价格到剪贴板: $\(priceString)")
|
||||
} else {
|
||||
print("❌ 无法获取 \(symbol) 价格")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let priceString = formatPriceWithCommas(price)
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString("$\(priceString)", forType: .string)
|
||||
|
||||
print("✅ 已复制 \(symbol) 价格到剪贴板: $\(priceString)")
|
||||
}
|
||||
}
|
||||
|
||||
// 选择币种的辅助方法
|
||||
private func selectSymbol(data: [String: Any], isCustom: Bool, displayName: String) {
|
||||
if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol {
|
||||
// 选择自定义币种 - 找到对应的索引并选择
|
||||
if let index = appSettings.customCryptoSymbols.firstIndex(of: customSymbol) {
|
||||
appSettings.selectCustomCryptoSymbol(at: index)
|
||||
print("✅ 已切换到自定义币种: \(displayName)")
|
||||
}
|
||||
|
||||
// 立即更新价格管理器和UI
|
||||
self.priceManager.updateCryptoSymbolSettings()
|
||||
// 使用0.0价格强制更新显示状态,确保图标和文字都正确更新
|
||||
self.updateMenuBarTitle(price: 0.0)
|
||||
} else if let symbol = data["symbol"] as? CryptoSymbol {
|
||||
// 选择默认币种
|
||||
appSettings.saveSelectedSymbol(symbol)
|
||||
print("✅ 已切换到默认币种: \(displayName)")
|
||||
|
||||
// 立即更新价格管理器和UI
|
||||
self.priceManager.updateCryptoSymbolSettings()
|
||||
// 使用0.0价格强制更新显示状态,确保图标和文字都正确更新
|
||||
self.updateMenuBarTitle(price: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,26 @@ import Foundation
|
||||
import Combine
|
||||
import ServiceManagement
|
||||
|
||||
/// Option+点击操作类型枚举
|
||||
/// 定义用户按住Option键点击币种时可以执行的操作
|
||||
enum OptionClickAction: String, CaseIterable, Codable {
|
||||
case copyPrice = "copyPrice"
|
||||
case openSpotTrading = "openSpotTrading"
|
||||
case openFuturesTrading = "openFuturesTrading"
|
||||
|
||||
/// 获取操作的显示名称
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .copyPrice:
|
||||
return "复制价格"
|
||||
case .openSpotTrading:
|
||||
return "Binance现货交易"
|
||||
case .openFuturesTrading:
|
||||
return "Binance合约交易"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用配置管理类
|
||||
/// 负责管理用户的刷新间隔设置和其他应用配置
|
||||
@MainActor
|
||||
@@ -45,6 +65,11 @@ class AppSettings: ObservableObject {
|
||||
/// 代理认证密码
|
||||
@Published var proxyPassword: String = ""
|
||||
|
||||
// MARK: - Option+点击功能设置
|
||||
|
||||
/// Option+左键点击的操作类型
|
||||
@Published var optionClickAction: OptionClickAction = .copyPrice
|
||||
|
||||
// MARK: - Private Properties
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
@@ -66,6 +91,10 @@ class AppSettings: ObservableObject {
|
||||
private let proxyUsernameKey = "ProxyUsername"
|
||||
private let proxyPasswordKey = "ProxyPassword"
|
||||
|
||||
// MARK: - Option+点击功能配置键值
|
||||
|
||||
private let optionClickActionKey = "OptionClickAction"
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
@@ -178,6 +207,20 @@ class AppSettings: ObservableObject {
|
||||
proxyUsername = defaults.string(forKey: proxyUsernameKey) ?? ""
|
||||
proxyPassword = defaults.string(forKey: proxyPasswordKey) ?? ""
|
||||
|
||||
// 加载Option+点击功能设置
|
||||
if let optionClickActionRaw = defaults.string(forKey: optionClickActionKey),
|
||||
let savedAction = OptionClickAction(rawValue: optionClickActionRaw) {
|
||||
optionClickAction = savedAction
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ✅ 已加载Option+点击功能: \(savedAction.displayName)")
|
||||
#endif
|
||||
} else {
|
||||
optionClickAction = .copyPrice
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ❌ 未找到有效Option+点击功能配置,使用默认值: \(optionClickAction.displayName)")
|
||||
#endif
|
||||
}
|
||||
|
||||
// 检查实际的自启动状态并同步
|
||||
checkAndSyncLaunchAtLoginStatus()
|
||||
|
||||
@@ -185,7 +228,7 @@ class AppSettings: ObservableObject {
|
||||
let proxyInfo = proxyEnabled ? "\(proxyHost):\(proxyPort)" : "未启用"
|
||||
let authInfo = proxyEnabled && !proxyUsername.isEmpty ? " (认证: \(proxyUsername))" : ""
|
||||
let customInfo = useCustomSymbol && !customCryptoSymbols.isEmpty ? " (自定义: \(customCryptoSymbols.count)个)" : ""
|
||||
print("🔧 [AppSettings] 配置加载完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(getCurrentActiveDisplayName())\(customInfo), 开机自启动: \(launchAtLogin), 代理: \(proxyInfo)\(authInfo)")
|
||||
print("🔧 [AppSettings] 配置加载完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(getCurrentActiveDisplayName())\(customInfo), 开机自启动: \(launchAtLogin), 代理: \(proxyInfo)\(authInfo), Option+点击: \(optionClickAction.displayName)")
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -223,8 +266,12 @@ class AppSettings: ObservableObject {
|
||||
defaults.set("", forKey: proxyUsernameKey)
|
||||
defaults.set("", forKey: proxyPasswordKey)
|
||||
|
||||
// 重置Option+点击功能设置
|
||||
optionClickAction = .copyPrice
|
||||
defaults.set(optionClickAction.rawValue, forKey: optionClickActionKey)
|
||||
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] 重置完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName), 自定义币种: 已清除, 代理: 已重置")
|
||||
print("🔧 [AppSettings] 重置完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName), 自定义币种: 已清除, 代理: 已重置, Option+点击: \(optionClickAction.displayName)")
|
||||
#endif
|
||||
|
||||
// 重置开机自启动设置
|
||||
@@ -344,6 +391,19 @@ class AppSettings: ObservableObject {
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Option+点击功能相关方法
|
||||
|
||||
/// 保存Option+点击功能设置
|
||||
/// - Parameter action: 要保存的操作类型
|
||||
func saveOptionClickAction(_ action: OptionClickAction) {
|
||||
optionClickAction = action
|
||||
defaults.set(action.rawValue, forKey: optionClickActionKey)
|
||||
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] 保存Option+点击功能设置: \(action.displayName)")
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - 开机自启动相关方法
|
||||
|
||||
/// 切换开机自启动状态
|
||||
|
||||
72
Bitcoin-Monitoring/Utils/BinanceURLGenerator.swift
Normal file
72
Bitcoin-Monitoring/Utils/BinanceURLGenerator.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// BinanceURLGenerator.swift
|
||||
// Bitcoin Monitoring
|
||||
//
|
||||
// Created by Mark on 2025/11/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
/**
|
||||
* 币安URL生成器
|
||||
* 负责生成币安现货和合约交易页面的URL
|
||||
*/
|
||||
struct BinanceURLGenerator {
|
||||
|
||||
/// 生成币安现货交易页面URL
|
||||
/// - Parameter symbol: 币种符号(如 BTC、ETH)
|
||||
/// - Returns: 现货交易页面URL字符串
|
||||
static func generateSpotTradingURL(for symbol: String) -> String {
|
||||
return "https://www.binance.com/zh-CN/trade/\(symbol)_USDT?type=spot"
|
||||
}
|
||||
|
||||
/// 生成币安合约交易页面URL
|
||||
/// - Parameter symbol: 币种符号(如 BTC、ETH)
|
||||
/// - Returns: 合约交易页面URL字符串
|
||||
static func generateFuturesTradingURL(for symbol: String) -> String {
|
||||
return "https://www.binance.com/zh-CN/futures/\(symbol)USDT"
|
||||
}
|
||||
|
||||
/// 打开币安现货交易页面
|
||||
/// - Parameter symbol: 币种符号
|
||||
/// - Returns: 是否成功打开页面
|
||||
static func openSpotTradingPage(for symbol: String) -> Bool {
|
||||
let url = generateSpotTradingURL(for: symbol)
|
||||
return openURL(url)
|
||||
}
|
||||
|
||||
/// 打开币安合约交易页面
|
||||
/// - Parameter symbol: 币种符号
|
||||
/// - Returns: 是否成功打开页面
|
||||
static func openFuturesTradingPage(for symbol: String) -> Bool {
|
||||
let url = generateFuturesTradingURL(for: symbol)
|
||||
return openURL(url)
|
||||
}
|
||||
|
||||
/// 使用系统默认浏览器打开URL
|
||||
/// - Parameter urlString: URL字符串
|
||||
/// - Returns: 是否成功打开
|
||||
private static func openURL(_ urlString: String) -> Bool {
|
||||
guard let url = URL(string: urlString) else {
|
||||
#if DEBUG
|
||||
print("❌ [BinanceURLGenerator] 无效的URL: \(urlString)")
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
|
||||
// 使用NSWorkspace打开URL(macOS专用)
|
||||
if #available(macOS 10.15, *) {
|
||||
NSWorkspace.shared.open(url)
|
||||
} else {
|
||||
// 对于较老的macOS版本,使用NSWorkspace的旧API
|
||||
let workspace = NSWorkspace.shared
|
||||
workspace.open(url)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
print("✅ [BinanceURLGenerator] 已打开URL: \(urlString)")
|
||||
#endif
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -147,6 +147,10 @@ struct AboutWindowView: View {
|
||||
|
||||
FeatureRow(icon: "exclamationmark.triangle.fill", title: "智能重试机制", description: "网络错误自动恢复")
|
||||
|
||||
FeatureRow(icon: "network", title: "支持代理服务器", description: "适用于受限网络环境")
|
||||
|
||||
FeatureRow(icon: "bell.fill", title: "更多功能", description: "等待你的发掘!")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +201,7 @@ struct AboutWindowView: View {
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.frame(width: 420, height: 500)
|
||||
.frame(width: 420, height: 590) // 设置固定高度以适应内容
|
||||
.alert("检测更新", isPresented: $showingUpdateAlert) {
|
||||
Button("确定", role: .cancel) {
|
||||
// 如果消息中包含"发现新版本",则打开发布页面并关闭窗口
|
||||
|
||||
@@ -53,6 +53,7 @@ struct PreferencesWindowView: View {
|
||||
@State private var tempProxyUsername: String
|
||||
@State private var tempProxyPassword: String
|
||||
@State private var tempLaunchAtLogin: Bool
|
||||
@State private var tempOptionClickAction: OptionClickAction
|
||||
|
||||
// 验证状态
|
||||
@State private var showingValidationError = false
|
||||
@@ -101,6 +102,7 @@ struct PreferencesWindowView: View {
|
||||
self._tempProxyUsername = State(initialValue: appSettings.proxyUsername)
|
||||
self._tempProxyPassword = State(initialValue: appSettings.proxyPassword)
|
||||
self._tempLaunchAtLogin = State(initialValue: appSettings.launchAtLogin)
|
||||
self._tempOptionClickAction = State(initialValue: appSettings.optionClickAction)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -226,11 +228,12 @@ struct PreferencesWindowView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 通用设置视图(刷新间隔 + 启动设置)
|
||||
// 通用设置视图(刷新间隔 + 启动设置 + Option+点击功能)
|
||||
private var generalSettingsView: some View {
|
||||
VStack(spacing: 24) {
|
||||
refreshSettingsView
|
||||
launchSettingsView
|
||||
optionClickSettingsView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +285,37 @@ struct PreferencesWindowView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Option+点击功能设置视图
|
||||
private var optionClickSettingsView: some View {
|
||||
SettingsGroupView(title: "Option+点击功能", icon: "cursorarrow.click.2") {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("按住Option+左键功能")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("设置按住Option键点击币种时执行的操作")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// 使用Picker让用户选择操作类型
|
||||
Picker("Option+点击操作", selection: $tempOptionClickAction) {
|
||||
ForEach(OptionClickAction.allCases, id: \.self) { action in
|
||||
Text(action.displayName).tag(action)
|
||||
}
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle())
|
||||
.frame(width: 180)
|
||||
.labelsHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 代理设置视图
|
||||
private var proxySettingsView: some View {
|
||||
SettingsGroupView(title: "代理设置", icon: "network") {
|
||||
@@ -681,6 +715,12 @@ struct PreferencesWindowView: View {
|
||||
print("✅ [Preferences] 已设置开机自启动: \(tempLaunchAtLogin)")
|
||||
}
|
||||
|
||||
// 保存Option+点击功能设置
|
||||
if tempOptionClickAction != appSettings.optionClickAction {
|
||||
appSettings.saveOptionClickAction(tempOptionClickAction)
|
||||
print("✅ [Preferences] 已保存Option+点击功能: \(tempOptionClickAction.displayName)")
|
||||
}
|
||||
|
||||
// 保存代理设置
|
||||
let port = Int(tempProxyPort) ?? 3128
|
||||
appSettings.saveProxySettings(
|
||||
|
||||
33
README.md
33
README.md
@@ -36,6 +36,7 @@
|
||||
- **价格复制功能**: 支持一键复制当前价格到剪贴板
|
||||
- **配置持久化**: 用户设置自动保存,重启后保持配置
|
||||
- **开机自启动**: 可选是否开机自动启动APP
|
||||
- **代理支持**: 支持 HTTP/HTTPS 代理配置,支持代理认证
|
||||
|
||||
### 🎨 用户体验
|
||||
- **中文界面**: 完整的中文用户界面
|
||||
@@ -67,7 +68,7 @@
|
||||
- **部署目标**: macOS 13.1
|
||||
|
||||
### 网络要求
|
||||
- 需要稳定的互联网连接
|
||||
- 需要稳定的互联网连接,国内用户建议使用科学上网工具 或 设置代理服务器。
|
||||
- 访问币安 API (`https://api.binance.com`) 的网络权限
|
||||
|
||||
## 🚀 快速开始
|
||||
@@ -124,24 +125,6 @@ xcodebuild -project "Bitcoin Monitoring.xcodeproj" -scheme "Bitcoin Monitoring"
|
||||
- **自定义币种组件**: 专门的币种管理界面,支持添加、删除和切换自定义币种
|
||||
- **图标缓存系统**: `CryptoIconGenerator` 实现图标生成和缓存机制,避免重复生成
|
||||
|
||||
### 并发处理
|
||||
|
||||
```swift
|
||||
// 主线程 UI 更新
|
||||
@MainActor
|
||||
class BTCMenuBarApp: ObservableObject
|
||||
|
||||
// 异步网络请求
|
||||
func fetchPrice() async throws -> Double
|
||||
|
||||
// Combine 响应式流
|
||||
priceManager.$currentPrice
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] price in
|
||||
self?.updateMenuBarTitle(price: price)
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 API 集成
|
||||
|
||||
### 币安 API 端点
|
||||
@@ -161,7 +144,7 @@ GET https://api.binance.com/api/v3/ticker/price?symbol={SYMBOL}
|
||||
- **网络异常处理**: 用户友好的错误提示和状态显示
|
||||
- **代理配置支持**: 完整的 HTTP/HTTPS 代理配置和连接测试
|
||||
|
||||
## ⚙️ 配置管理
|
||||
## ⚙️ 配置管理(持久化配置)
|
||||
|
||||
### UserDefaults 键值
|
||||
- `BTCRefreshInterval`: 刷新间隔设置
|
||||
@@ -171,9 +154,7 @@ GET https://api.binance.com/api/v3/ticker/price?symbol={SYMBOL}
|
||||
- `SelectedCustomSymbolIndex`: 当前选中的自定义币种索引
|
||||
- `UseCustomSymbol`: 是否使用自定义币种
|
||||
- `ProxyEnabled/ProxyHost/ProxyPort/ProxyUsername/ProxyPassword`: 代理设置(包括认证)
|
||||
|
||||
### 配置持久化
|
||||
所有用户设置通过 `AppSettings` 类的 `@Published` 属性自动保存到 UserDefaults,应用重启后保持配置。使用 Combine 框架实现配置变化的实时响应。
|
||||
- `OptionClickAction`: 选项点击操作设置
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
@@ -184,10 +165,12 @@ GET https://api.binance.com/api/v3/ticker/price?symbol={SYMBOL}
|
||||
**问题**: 双击应用图标无反应
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 指定该命令以启动APP
|
||||
# 执行该命令以启动APP
|
||||
sudo xattr -d com.apple.quarantine "/Applications/Bitcoin Monitoring.app"
|
||||
|
||||
# 或者在系统偏好设置中允许应用运行
|
||||
# 或者
|
||||
|
||||
# 系统偏好设置中允许应用运行
|
||||
系统设置 → 隐私与安全性 → 安全性 → 已阻止“Bitcoin Monitoring.app”以保护Mac → 仍要打开
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user