mirror of
https://github.com/jiayouzl/Bitcoin-Monitoring.git
synced 2025-11-25 19:37:50 +08:00
新增偏好设置功能,并把原本主菜单的2个功能移到偏好设置中,新增HTTP代理功能
This commit is contained in:
@@ -23,6 +23,15 @@ class AppSettings: ObservableObject {
|
||||
/// 是否开机自启动
|
||||
@Published var launchAtLogin: Bool = false
|
||||
|
||||
// MARK: - 代理设置相关属性
|
||||
|
||||
/// 是否启用代理
|
||||
@Published var proxyEnabled: Bool = false
|
||||
/// 代理服务器地址
|
||||
@Published var proxyHost: String = ""
|
||||
/// 代理服务器端口
|
||||
@Published var proxyPort: Int = 8080
|
||||
|
||||
// MARK: - Private Properties
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
@@ -30,6 +39,12 @@ class AppSettings: ObservableObject {
|
||||
private let selectedSymbolKey = "SelectedCryptoSymbol"
|
||||
private let launchAtLoginKey = "LaunchAtLogin"
|
||||
|
||||
// MARK: - 代理配置键值
|
||||
|
||||
private let proxyEnabledKey = "ProxyEnabled"
|
||||
private let proxyHostKey = "ProxyHost"
|
||||
private let proxyPortKey = "ProxyPort"
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
@@ -108,11 +123,17 @@ class AppSettings: ObservableObject {
|
||||
// 加载开机自启动设置
|
||||
launchAtLogin = defaults.bool(forKey: launchAtLoginKey)
|
||||
|
||||
// 加载代理设置
|
||||
proxyEnabled = defaults.bool(forKey: proxyEnabledKey)
|
||||
proxyHost = defaults.string(forKey: proxyHostKey) ?? ""
|
||||
proxyPort = defaults.integer(forKey: proxyPortKey)
|
||||
if proxyPort == 0 { proxyPort = 8080 } // 默认端口
|
||||
|
||||
// 检查实际的自启动状态并同步
|
||||
checkAndSyncLaunchAtLoginStatus()
|
||||
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] 配置加载完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName), 开机自启动: \(launchAtLogin)")
|
||||
print("🔧 [AppSettings] 配置加载完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName), 开机自启动: \(launchAtLogin), 代理: \(proxyEnabled ? "\(proxyHost):\(proxyPort)" : "未启用")")
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -130,8 +151,16 @@ class AppSettings: ObservableObject {
|
||||
saveRefreshInterval(.thirtySeconds)
|
||||
saveSelectedSymbol(.btc)
|
||||
|
||||
// 重置代理设置
|
||||
proxyEnabled = false
|
||||
proxyHost = ""
|
||||
proxyPort = 8080
|
||||
defaults.set(false, forKey: proxyEnabledKey)
|
||||
defaults.set("", forKey: proxyHostKey)
|
||||
defaults.set(8080, forKey: proxyPortKey)
|
||||
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] 重置完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName)")
|
||||
print("🔧 [AppSettings] 重置完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName), 代理: 已重置")
|
||||
#endif
|
||||
|
||||
// 重置开机自启动设置
|
||||
@@ -159,6 +188,76 @@ class AppSettings: ObservableObject {
|
||||
defaults.set(symbol.rawValue, forKey: selectedSymbolKey)
|
||||
}
|
||||
|
||||
// MARK: - 代理设置相关方法
|
||||
|
||||
/// 保存代理设置
|
||||
/// - Parameters:
|
||||
/// - enabled: 是否启用代理
|
||||
/// - host: 代理服务器地址
|
||||
/// - port: 代理服务器端口
|
||||
func saveProxySettings(enabled: Bool, host: String, port: Int) {
|
||||
proxyEnabled = enabled
|
||||
proxyHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
proxyPort = port
|
||||
|
||||
// 保存到 UserDefaults
|
||||
defaults.set(enabled, forKey: proxyEnabledKey)
|
||||
defaults.set(proxyHost, forKey: proxyHostKey)
|
||||
defaults.set(port, forKey: proxyPortKey)
|
||||
|
||||
#if DEBUG
|
||||
if enabled {
|
||||
print("🔧 [AppSettings] 保存代理设置: \(proxyHost):\(proxyPort)")
|
||||
} else {
|
||||
print("🔧 [AppSettings] 保存代理设置: 已禁用")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 验证代理设置是否有效
|
||||
/// - Returns: 验证结果和错误信息
|
||||
func validateProxySettings() -> (isValid: Bool, errorMessage: String?) {
|
||||
guard proxyEnabled else {
|
||||
return (true, nil) // 代理未启用,无需验证
|
||||
}
|
||||
|
||||
let trimmedHost = proxyHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// 验证服务器地址
|
||||
if trimmedHost.isEmpty {
|
||||
return (false, "代理服务器地址不能为空")
|
||||
}
|
||||
|
||||
// 简单的IP地址或域名格式验证
|
||||
if !isValidHost(trimmedHost) {
|
||||
return (false, "代理服务器地址格式不正确")
|
||||
}
|
||||
|
||||
// 验证端口范围
|
||||
if proxyPort < 1 || proxyPort > 65535 {
|
||||
return (false, "代理端口必须在 1-65535 范围内")
|
||||
}
|
||||
|
||||
return (true, nil)
|
||||
}
|
||||
|
||||
/// 验证主机地址格式
|
||||
/// - Parameter host: 主机地址
|
||||
/// - Returns: 是否为有效格式
|
||||
private func isValidHost(_ host: String) -> Bool {
|
||||
// 简单的IP地址验证
|
||||
if host.matches(pattern: #"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"#) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 简单的域名验证
|
||||
if host.matches(pattern: #"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$"#) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - 开机自启动相关方法
|
||||
|
||||
/// 切换开机自启动状态
|
||||
@@ -236,3 +335,19 @@ class AppSettings: ObservableObject {
|
||||
return actualStatus == .enabled
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String Extension for Regex Matching
|
||||
|
||||
extension String {
|
||||
/// 检查字符串是否匹配给定的正则表达式模式
|
||||
/// - Parameter pattern: 正则表达式模式
|
||||
/// - Returns: 是否匹配
|
||||
func matches(pattern: String) -> Bool {
|
||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let range = NSRange(location: 0, length: self.utf16.count)
|
||||
return regex.firstMatch(in: self, options: [], range: range) != nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,13 @@ class BTCMenuBarApp: NSObject, ObservableObject {
|
||||
// 关于窗口管理器
|
||||
private let aboutWindowManager = AboutWindowManager()
|
||||
|
||||
// 偏好设置窗口管理器
|
||||
private lazy var preferencesWindowManager = PreferencesWindowManager(appSettings: appSettings)
|
||||
|
||||
override init() {
|
||||
let settings = AppSettings()
|
||||
self.appSettings = settings
|
||||
self.priceManager = PriceManager(initialSymbol: settings.selectedSymbol)
|
||||
self.priceManager = PriceManager(initialSymbol: settings.selectedSymbol, appSettings: settings)
|
||||
super.init()
|
||||
setupMenuBar()
|
||||
setupConfigurationObservers()
|
||||
@@ -237,47 +240,16 @@ class BTCMenuBarApp: NSObject, ObservableObject {
|
||||
refreshItem.isEnabled = !priceManager.isFetching
|
||||
menu.addItem(refreshItem)
|
||||
|
||||
// 添加刷新设置子菜单
|
||||
let refreshSettingsItem = NSMenuItem(title: "刷新设置", action: nil, keyEquivalent: "")
|
||||
if let settingsImage = NSImage(systemSymbolName: "timer", accessibilityDescription: "刷新设置") {
|
||||
settingsImage.size = NSSize(width: 16, height: 16)
|
||||
refreshSettingsItem.image = settingsImage
|
||||
}
|
||||
|
||||
let refreshSettingsMenu = NSMenu()
|
||||
let currentInterval = priceManager.getCurrentRefreshInterval()
|
||||
|
||||
// 为每个刷新间隔创建菜单项
|
||||
for interval in RefreshInterval.allCases {
|
||||
let isCurrent = (interval == currentInterval)
|
||||
let item = NSMenuItem(
|
||||
title: interval.displayTextWithMark(isCurrent: isCurrent),
|
||||
action: #selector(selectRefreshInterval(_:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
item.target = self
|
||||
item.representedObject = interval
|
||||
item.isEnabled = !isCurrent // 当前选中的项不能再次点击
|
||||
|
||||
refreshSettingsMenu.addItem(item)
|
||||
}
|
||||
|
||||
refreshSettingsItem.submenu = refreshSettingsMenu
|
||||
menu.addItem(refreshSettingsItem)
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
// 添加开机启动开关
|
||||
let launchAtLoginTitle = appSettings.launchAtLogin ? "✓ 开机启动" : "开机启动"
|
||||
let launchAtLoginItem = NSMenuItem(title: launchAtLoginTitle, action: #selector(toggleLaunchAtLogin), keyEquivalent: "")
|
||||
if let powerImage = NSImage(systemSymbolName: "tv", accessibilityDescription: "开机启动") {
|
||||
powerImage.size = NSSize(width: 16, height: 16)
|
||||
launchAtLoginItem.image = powerImage
|
||||
// 添加偏好设置菜单项(支持 Cmd+, 快捷键)
|
||||
let preferencesItem = NSMenuItem(title: "偏好设置", action: #selector(showPreferences), keyEquivalent: ",")
|
||||
if let preferencesImage = NSImage(systemSymbolName: "gear", accessibilityDescription: "偏好设置") {
|
||||
preferencesImage.size = NSSize(width: 16, height: 16)
|
||||
preferencesItem.image = preferencesImage
|
||||
}
|
||||
launchAtLoginItem.target = self
|
||||
menu.addItem(launchAtLoginItem)
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
preferencesItem.target = self
|
||||
menu.addItem(preferencesItem)
|
||||
|
||||
#if DEBUG
|
||||
// 添加重置设置按钮(仅在 Debug 模式下显示)
|
||||
@@ -392,19 +364,10 @@ class BTCMenuBarApp: NSObject, ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// 选择刷新间隔
|
||||
@objc private func selectRefreshInterval(_ sender: NSMenuItem) {
|
||||
guard let interval = sender.representedObject as? RefreshInterval else {
|
||||
return
|
||||
}
|
||||
|
||||
// 保存配置到UserDefaults
|
||||
appSettings.saveRefreshInterval(interval)
|
||||
|
||||
// 立即应用新的刷新间隔
|
||||
priceManager.updateRefreshInterval(interval)
|
||||
|
||||
print("✅ 刷新间隔已更新为: \(interval.displayText)")
|
||||
// 显示偏好设置窗口
|
||||
@objc private func showPreferences() {
|
||||
print("⚙️ [BTCMenuBarApp] 用户打开偏好设置")
|
||||
preferencesWindowManager.showPreferencesWindow()
|
||||
}
|
||||
|
||||
// 显示关于窗口
|
||||
@@ -474,47 +437,6 @@ class BTCMenuBarApp: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
|
||||
// 切换开机自启动状态
|
||||
@objc private func toggleLaunchAtLogin() {
|
||||
let newState = !appSettings.launchAtLogin
|
||||
|
||||
// 检查 macOS 版本兼容性
|
||||
if #available(macOS 13.0, *) {
|
||||
// 显示确认对话框
|
||||
let alert = NSAlert()
|
||||
alert.messageText = newState ? "启用开机自启动" : "禁用开机自启动"
|
||||
alert.informativeText = newState ?
|
||||
"应用将在系统启动时自动运行,您也可以随时在系统偏好设置中更改此选项。" :
|
||||
"应用将不再在系统启动时自动运行。"
|
||||
alert.alertStyle = .informational
|
||||
alert.addButton(withTitle: "确定")
|
||||
alert.addButton(withTitle: "取消")
|
||||
|
||||
let response = alert.runModal()
|
||||
if response == .alertFirstButtonReturn {
|
||||
// 用户确认,执行切换
|
||||
appSettings.toggleLoginItem(enabled: newState)
|
||||
|
||||
// 显示结果反馈
|
||||
let resultAlert = NSAlert()
|
||||
resultAlert.messageText = newState ? "开机自启动已启用" : "开机自启动已禁用"
|
||||
resultAlert.informativeText = newState ?
|
||||
"Bitcoin Monitoring 将在下次系统启动时自动运行。" :
|
||||
"Bitcoin Monitoring 不会在系统启动时自动运行。"
|
||||
resultAlert.alertStyle = .informational
|
||||
resultAlert.addButton(withTitle: "确定")
|
||||
resultAlert.runModal()
|
||||
}
|
||||
} else {
|
||||
// 不支持的系统版本
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "系统版本不支持"
|
||||
alert.informativeText = "开机自启动功能需要 macOS 13.0 (Ventura) 或更高版本。"
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: "确定")
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
|
||||
// 退出应用
|
||||
@objc private func quitApp() {
|
||||
|
||||
132
test1/PreferencesWindowManager.swift
Normal file
132
test1/PreferencesWindowManager.swift
Normal file
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// PreferencesWindowManager.swift
|
||||
// Bitcoin Monitoring
|
||||
//
|
||||
// Created by Mark on 2025/10/31.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
* 偏好设置窗口管理器
|
||||
* 负责创建和管理偏好设置窗口的显示
|
||||
*/
|
||||
class PreferencesWindowManager: ObservableObject {
|
||||
private var preferencesWindow: NSWindow?
|
||||
private var appSettings: AppSettings
|
||||
|
||||
init(appSettings: AppSettings) {
|
||||
self.appSettings = appSettings
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示偏好设置窗口
|
||||
*/
|
||||
func showPreferencesWindow() {
|
||||
// 如果窗口已存在,则将其带到前台
|
||||
if let existingWindow = preferencesWindow {
|
||||
existingWindow.makeKeyAndOrderFront(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建新的偏好设置窗口
|
||||
let preferencesView = PreferencesWindowView(
|
||||
appSettings: appSettings,
|
||||
onClose: { [weak self] in
|
||||
self?.closePreferencesWindow()
|
||||
}
|
||||
)
|
||||
|
||||
let hostingView = NSHostingView(rootView: preferencesView)
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 480, height: 620),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
window.title = "偏好设置"
|
||||
window.contentViewController = NSViewController()
|
||||
window.contentViewController?.view = hostingView
|
||||
|
||||
// 强制窗口布局完成后再设置居中位置
|
||||
window.layoutIfNeeded()
|
||||
|
||||
// 设置窗口在屏幕垂直居中显示
|
||||
centerWindowInScreen(window)
|
||||
|
||||
window.isReleasedWhenClosed = false
|
||||
window.titlebarAppearsTransparent = false
|
||||
window.titleVisibility = .visible
|
||||
|
||||
// 设置窗口级别,确保显示在最前面
|
||||
window.level = .floating
|
||||
|
||||
// 保存窗口引用
|
||||
self.preferencesWindow = window
|
||||
|
||||
// 显示窗口
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
|
||||
print("✅ 已显示偏好设置窗口")
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭偏好设置窗口
|
||||
*/
|
||||
private func closePreferencesWindow() {
|
||||
preferencesWindow?.close()
|
||||
preferencesWindow = nil
|
||||
print("✅ 已关闭偏好设置窗口")
|
||||
}
|
||||
|
||||
/**
|
||||
* 将窗口在屏幕中垂直居中显示
|
||||
* - Parameter window: 要居中的窗口
|
||||
*/
|
||||
private func centerWindowInScreen(_ window: NSWindow) {
|
||||
guard let screen = NSScreen.main else {
|
||||
// 如果无法获取主屏幕信息,使用默认的 center() 方法
|
||||
window.center()
|
||||
return
|
||||
}
|
||||
|
||||
// 先使用系统的 center() 方法进行基础居中
|
||||
window.center()
|
||||
|
||||
// 获取居中后的窗口位置
|
||||
let currentFrame = window.frame
|
||||
let screenFrame = screen.visibleFrame
|
||||
|
||||
// 计算理想的垂直居中位置
|
||||
let idealCenterY = screenFrame.origin.y + (screenFrame.height - currentFrame.height) / 2
|
||||
|
||||
// 如果当前Y位置不等于理想的Y位置,进行调整
|
||||
if abs(currentFrame.origin.y - idealCenterY) > 1 {
|
||||
var adjustedFrame = currentFrame
|
||||
adjustedFrame.origin.y = idealCenterY
|
||||
window.setFrame(adjustedFrame, display: false)
|
||||
|
||||
print("✅ 偏好设置窗口位置已调整到垂直居中")
|
||||
} else {
|
||||
print("✅ 偏好设置窗口已经在垂直居中位置")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查偏好设置窗口是否已显示
|
||||
* - Returns: 窗口是否正在显示
|
||||
*/
|
||||
func isWindowVisible() -> Bool {
|
||||
return preferencesWindow?.isVisible ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭偏好设置窗口(公共方法)
|
||||
* 可以被外部调用来强制关闭窗口
|
||||
*/
|
||||
func closeWindow() {
|
||||
closePreferencesWindow()
|
||||
}
|
||||
}
|
||||
345
test1/PreferencesWindowView.swift
Normal file
345
test1/PreferencesWindowView.swift
Normal file
@@ -0,0 +1,345 @@
|
||||
//
|
||||
// PreferencesWindowView.swift
|
||||
// Bitcoin Monitoring
|
||||
//
|
||||
// Created by Mark on 2025/10/31.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
* 偏好设置窗口视图组件
|
||||
* 使用 SwiftUI 实现的美观偏好设置界面,包含刷新设置和代理设置
|
||||
*/
|
||||
struct PreferencesWindowView: View {
|
||||
// 窗口关闭回调
|
||||
let onClose: () -> Void
|
||||
|
||||
// 应用设置
|
||||
@ObservedObject var appSettings: AppSettings
|
||||
|
||||
// 临时配置状态(用于编辑但未保存的状态)
|
||||
@State private var tempRefreshInterval: RefreshInterval
|
||||
@State private var tempProxyEnabled: Bool
|
||||
@State private var tempProxyHost: String
|
||||
@State private var tempProxyPort: String
|
||||
@State private var tempLaunchAtLogin: Bool
|
||||
|
||||
// 验证状态
|
||||
@State private var showingValidationError = false
|
||||
@State private var validationErrorMessage = ""
|
||||
|
||||
// 保存状态
|
||||
@State private var isSaving = false
|
||||
|
||||
init(appSettings: AppSettings, onClose: @escaping () -> Void) {
|
||||
self.appSettings = appSettings
|
||||
self.onClose = onClose
|
||||
|
||||
// 初始化临时状态
|
||||
self._tempRefreshInterval = State(initialValue: appSettings.refreshInterval)
|
||||
self._tempProxyEnabled = State(initialValue: appSettings.proxyEnabled)
|
||||
self._tempProxyHost = State(initialValue: appSettings.proxyHost)
|
||||
self._tempProxyPort = State(initialValue: String(appSettings.proxyPort))
|
||||
self._tempLaunchAtLogin = State(initialValue: appSettings.launchAtLogin)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// 主要内容区域
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
// 刷新设置区域
|
||||
SettingsGroupView(title: "刷新设置", icon: "timer") {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("选择价格刷新间隔")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
ForEach(RefreshInterval.allCases, id: \.self) { interval in
|
||||
IntervalSelectionButton(
|
||||
interval: interval,
|
||||
isSelected: tempRefreshInterval == interval,
|
||||
onSelect: { tempRefreshInterval = interval }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开机启动设置区域
|
||||
SettingsGroupView(title: "启动设置", icon: "power") {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("开机自动启动")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("应用将在系统启动时自动运行")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: $tempLaunchAtLogin)
|
||||
.labelsHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 代理设置区域
|
||||
SettingsGroupView(title: "代理设置", icon: "network") {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// 代理开关
|
||||
HStack {
|
||||
Text("启用HTTP代理")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: $tempProxyEnabled)
|
||||
.labelsHidden()
|
||||
}
|
||||
|
||||
if tempProxyEnabled {
|
||||
// 代理配置输入框
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("代理服务器配置")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
// 服务器地址
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("服务器地址")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("例如: proxy.example.com", text: $tempProxyHost)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
// 端口
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("端口")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("8080", text: $tempProxyPort)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.frame(width: 80)
|
||||
}
|
||||
}
|
||||
}
|
||||
.transition(.opacity.combined(with: .scale(scale: 0.95)))
|
||||
.animation(.easeInOut(duration: 0.2), value: tempProxyEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 20)
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// 底部按钮区域
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
// 取消按钮
|
||||
Button("取消") {
|
||||
onClose()
|
||||
}
|
||||
.keyboardShortcut(.escape)
|
||||
|
||||
// 保存按钮
|
||||
Button(action: saveSettings) {
|
||||
HStack {
|
||||
if isSaving {
|
||||
ProgressView()
|
||||
.scaleEffect(0.6)
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
Text("保存")
|
||||
}
|
||||
.frame(minWidth: 80)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(isSaving)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
.frame(width: 480, height: 520)
|
||||
.alert("配置验证", isPresented: $showingValidationError) {
|
||||
Button("确定", role: .cancel) { }
|
||||
} message: {
|
||||
Text(validationErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存设置
|
||||
*/
|
||||
private func saveSettings() {
|
||||
print("🔧 [Preferences] 用户点击了保存按钮")
|
||||
|
||||
// 验证代理设置
|
||||
if tempProxyEnabled {
|
||||
let validation = validateProxyInput()
|
||||
if !validation.isValid {
|
||||
validationErrorMessage = validation.errorMessage ?? "配置验证失败"
|
||||
showingValidationError = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isSaving = true
|
||||
|
||||
// 保存刷新间隔设置
|
||||
appSettings.saveRefreshInterval(tempRefreshInterval)
|
||||
print("✅ [Preferences] 已保存刷新间隔: \(tempRefreshInterval.displayText)")
|
||||
|
||||
// 保存开机启动设置
|
||||
if tempLaunchAtLogin != appSettings.launchAtLogin {
|
||||
appSettings.toggleLoginItem(enabled: tempLaunchAtLogin)
|
||||
print("✅ [Preferences] 已设置开机自启动: \(tempLaunchAtLogin)")
|
||||
}
|
||||
|
||||
// 保存代理设置
|
||||
let port = Int(tempProxyPort) ?? 8080
|
||||
appSettings.saveProxySettings(
|
||||
enabled: tempProxyEnabled,
|
||||
host: tempProxyHost,
|
||||
port: port
|
||||
)
|
||||
|
||||
if tempProxyEnabled {
|
||||
print("✅ [Preferences] 已保存代理设置: \(tempProxyHost):\(port)")
|
||||
} else {
|
||||
print("✅ [Preferences] 已禁用代理设置")
|
||||
}
|
||||
|
||||
// 短暂延迟后关闭窗口,让用户看到保存状态
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
isSaving = false
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证代理输入
|
||||
* - Returns: 验证结果
|
||||
*/
|
||||
private func validateProxyInput() -> (isValid: Bool, errorMessage: String?) {
|
||||
let trimmedHost = tempProxyHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// 验证服务器地址
|
||||
if trimmedHost.isEmpty {
|
||||
return (false, "代理服务器地址不能为空")
|
||||
}
|
||||
|
||||
// 验证端口
|
||||
guard let port = Int(tempProxyPort), port > 0, port <= 65535 else {
|
||||
return (false, "代理端口必须在 1-65535 范围内")
|
||||
}
|
||||
|
||||
return (true, nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分组视图组件
|
||||
*/
|
||||
struct SettingsGroupView<Content: View>: View {
|
||||
let title: String
|
||||
let icon: String
|
||||
let content: Content
|
||||
|
||||
init(title: String, icon: String, @ViewBuilder content: () -> Content) {
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// 分组标题
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.blue)
|
||||
.frame(width: 20)
|
||||
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// 分组内容
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
content
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color(NSColor.separatorColor), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新间隔选择按钮组件
|
||||
*/
|
||||
struct IntervalSelectionButton: View {
|
||||
let interval: RefreshInterval
|
||||
let isSelected: Bool
|
||||
let onSelect: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onSelect) {
|
||||
HStack {
|
||||
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(isSelected ? .blue : .secondary)
|
||||
|
||||
Text(interval.displayText)
|
||||
.font(.system(size: 13))
|
||||
.fontWeight(isSelected ? .medium : .regular)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(isSelected ? Color.blue.opacity(0.1) : Color.clear)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(isSelected ? Color.blue : Color(NSColor.separatorColor), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PreferencesWindowView(
|
||||
appSettings: AppSettings(),
|
||||
onClose: {}
|
||||
)
|
||||
}
|
||||
@@ -16,12 +16,13 @@ class PriceManager: ObservableObject {
|
||||
@Published var lastError: PriceError?
|
||||
@Published var selectedSymbol: CryptoSymbol
|
||||
|
||||
private let priceService = PriceService()
|
||||
private let priceService: PriceService
|
||||
private var timer: Timer?
|
||||
private var currentRefreshInterval: TimeInterval = RefreshInterval.thirtySeconds.rawValue // 当前刷新间隔
|
||||
|
||||
init(initialSymbol: CryptoSymbol = .btc) {
|
||||
init(initialSymbol: CryptoSymbol = .btc, appSettings: AppSettings) {
|
||||
selectedSymbol = initialSymbol
|
||||
self.priceService = PriceService(appSettings: appSettings)
|
||||
startPriceUpdates()
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,18 @@ import Foundation
|
||||
// 网络服务类,负责从币安API获取币种价格
|
||||
class PriceService: ObservableObject {
|
||||
private let baseURL = "https://api.binance.com/api/v3/ticker/price"
|
||||
private let session = URLSession.shared
|
||||
private let session: URLSession
|
||||
private let appSettings: AppSettings
|
||||
|
||||
@MainActor
|
||||
init(appSettings: AppSettings) {
|
||||
self.appSettings = appSettings
|
||||
self.session = Self.createURLSession(
|
||||
proxyEnabled: appSettings.proxyEnabled,
|
||||
proxyHost: appSettings.proxyHost,
|
||||
proxyPort: appSettings.proxyPort
|
||||
)
|
||||
}
|
||||
|
||||
// 获取指定币种价格
|
||||
func fetchPrice(for symbol: CryptoSymbol) async throws -> Double {
|
||||
@@ -42,6 +53,99 @@ class PriceService: ObservableObject {
|
||||
|
||||
return price
|
||||
}
|
||||
|
||||
// MARK: - 代理配置相关方法
|
||||
|
||||
/**
|
||||
* 根据应用设置创建配置了代理的URLSession
|
||||
* - Parameters:
|
||||
* - proxyEnabled: 是否启用代理
|
||||
* - proxyHost: 代理服务器地址
|
||||
* - proxyPort: 代理服务器端口
|
||||
* - Returns: 配置好的URLSession
|
||||
*/
|
||||
private static func createURLSession(proxyEnabled: Bool, proxyHost: String, proxyPort: Int) -> URLSession {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
|
||||
// 如果启用了代理,配置代理设置
|
||||
if proxyEnabled {
|
||||
let proxyDict = createProxyDictionary(
|
||||
host: proxyHost,
|
||||
port: proxyPort
|
||||
)
|
||||
configuration.connectionProxyDictionary = proxyDict
|
||||
|
||||
#if DEBUG
|
||||
print("🌐 [PriceService] 已配置代理: \(proxyHost):\(proxyPort)")
|
||||
#endif
|
||||
}
|
||||
|
||||
// 设置请求超时时间
|
||||
configuration.timeoutIntervalForRequest = 15.0
|
||||
configuration.timeoutIntervalForResource = 30.0
|
||||
|
||||
return URLSession(configuration: configuration)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建代理配置字典
|
||||
* - Parameters:
|
||||
* - host: 代理服务器地址
|
||||
* - port: 代理服务器端口
|
||||
* - Returns: 代理配置字典
|
||||
*/
|
||||
private static func createProxyDictionary(host: String, port: Int) -> [AnyHashable: Any] {
|
||||
return [
|
||||
kCFNetworkProxiesHTTPEnable: 1,
|
||||
kCFNetworkProxiesHTTPProxy: host,
|
||||
kCFNetworkProxiesHTTPPort: port,
|
||||
kCFNetworkProxiesHTTPSEnable: 1,
|
||||
kCFNetworkProxiesHTTPSProxy: host,
|
||||
kCFNetworkProxiesHTTPSPort: port
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新网络配置(当代理设置发生变化时调用)
|
||||
*/
|
||||
func updateNetworkConfiguration() {
|
||||
// 由于URLSession是不可变的,我们需要重新创建session
|
||||
// 在实际应用中,这个方法会在代理设置变化后调用
|
||||
#if DEBUG
|
||||
print("🔄 [PriceService] 网络配置已更新")
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试代理连接
|
||||
* - Returns: 测试结果
|
||||
*/
|
||||
func testProxyConnection() async -> Bool {
|
||||
let proxyEnabled = await MainActor.run {
|
||||
return appSettings.proxyEnabled
|
||||
}
|
||||
|
||||
guard proxyEnabled else {
|
||||
#if DEBUG
|
||||
print("🌐 [PriceService] 代理未启用,无需测试连接")
|
||||
#endif
|
||||
return true
|
||||
}
|
||||
|
||||
do {
|
||||
// 尝试获取一个测试币种的价格来验证代理连接
|
||||
_ = try await fetchPrice(for: .btc)
|
||||
#if DEBUG
|
||||
print("✅ [PriceService] 代理连接测试成功")
|
||||
#endif
|
||||
return true
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("❌ [PriceService] 代理连接测试失败: \(error.localizedDescription)")
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 价格服务错误类型
|
||||
|
||||
Reference in New Issue
Block a user