diff --git a/Bitcoin Monitoring.xcodeproj/project.pbxproj b/Bitcoin Monitoring.xcodeproj/project.pbxproj index 998c5a6..6bfe837 100644 --- a/Bitcoin Monitoring.xcodeproj/project.pbxproj +++ b/Bitcoin Monitoring.xcodeproj/project.pbxproj @@ -78,7 +78,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1620; - LastUpgradeCheck = 1620; + LastUpgradeCheck = 2600; TargetAttributes = { 4E94106F2EB09F90003658CB = { CreatedOnToolsVersion = 16.2; @@ -159,6 +159,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -183,6 +184,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -222,6 +224,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -239,6 +242,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; @@ -253,8 +257,12 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"test1/Preview Content\""; + ENABLE_APP_SANDBOX = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -263,7 +271,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.4; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.mark.test1; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -281,8 +289,12 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"test1/Preview Content\""; + ENABLE_APP_SANDBOX = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -291,7 +303,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.4; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.mark.test1; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/test1/AppSettings.swift b/test1/AppSettings.swift new file mode 100644 index 0000000..d0985eb --- /dev/null +++ b/test1/AppSettings.swift @@ -0,0 +1,55 @@ +// +// AppSettings.swift +// Bitcoin Monitoring +// +// Created by Mark on 2025/10/29. +// + +import Foundation +import Combine + +/// 应用配置管理类 +/// 负责管理用户的刷新间隔设置和其他应用配置 +@MainActor +class AppSettings: ObservableObject { + + // MARK: - Published Properties + + /// 当前选中的刷新间隔 + @Published var refreshInterval: RefreshInterval = .thirtySeconds + + // MARK: - Private Properties + + private let defaults = UserDefaults.standard + private let refreshIntervalKey = "BTCRefreshInterval" + + // MARK: - Initialization + + init() { + loadSettings() + } + + // MARK: - Configuration Methods + + /// 从UserDefaults加载保存的配置 + /// 如果没有保存的配置,使用默认值(30秒) + func loadSettings() { + let savedValue = defaults.double(forKey: refreshIntervalKey) + + // 查找匹配的刷新间隔,如果没有匹配的则使用默认值 + if let savedInterval = RefreshInterval.allCases.first(where: { $0.rawValue == savedValue }) { + refreshInterval = savedInterval + } else { + refreshInterval = .thirtySeconds + // 保存默认值,确保下次启动时有正确的配置 + saveRefreshInterval(.thirtySeconds) + } + } + + /// 保存用户选择的刷新间隔 + /// - Parameter interval: 要保存的刷新间隔 + func saveRefreshInterval(_ interval: RefreshInterval) { + refreshInterval = interval + defaults.set(interval.rawValue, forKey: refreshIntervalKey) + } +} diff --git a/test1/BTCMenuBarApp.swift b/test1/BTCMenuBarApp.swift index 555b1f3..8199e82 100644 --- a/test1/BTCMenuBarApp.swift +++ b/test1/BTCMenuBarApp.swift @@ -2,7 +2,7 @@ // BTCMenuBarApp.swift // test1 // -// Created by zl_vm on 2025/10/28. +// Created by Mark on 2025/10/28. // import SwiftUI @@ -15,11 +15,23 @@ class BTCMenuBarApp: NSObject, ObservableObject { private var statusItem: NSStatusItem? private var popover: NSPopover? private var priceManager = PriceManager() + private var appSettings = AppSettings() private var cancellables = Set() override init() { super.init() setupMenuBar() + setupConfigurationObservers() + } + + // 设置配置观察者 + private func setupConfigurationObservers() { + // 监听刷新间隔配置变化 + appSettings.$refreshInterval + .sink { [weak self] newInterval in + self?.priceManager.updateRefreshInterval(newInterval) + } + .store(in: &cancellables) } // 设置菜单栏 @@ -134,8 +146,45 @@ 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()) + // 添加GitHub按钮(带GitHub图标) + let checkUpdateItem = NSMenuItem(title: "GitHub", action: #selector(checkForUpdates), keyEquivalent: "") + if let updateImage = NSImage(systemSymbolName: "star.circle", accessibilityDescription: "GitHub") { + updateImage.size = NSSize(width: 16, height: 16) + checkUpdateItem.image = updateImage + } + checkUpdateItem.target = self + menu.addItem(checkUpdateItem) + // 添加关于按钮(带信息图标) let aboutItem = NSMenuItem(title: "关于", action: #selector(showAbout), keyEquivalent: "") if let infoImage = NSImage(systemSymbolName: "info.circle", accessibilityDescription: "关于") { @@ -180,31 +229,70 @@ 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 showAbout() { + let currentInterval = priceManager.getCurrentRefreshInterval() + + // 获取应用版本信息 + let version = getAppVersion() let alert = NSAlert() - alert.messageText = "BTC价格监控器" + alert.messageText = "BTC价格监控器 v\(version)" alert.informativeText = """ - 🚀 一个专业的macOS菜单栏应用,用于实时显示BTC价格 + 🚀 一款 macOS 原生菜单栏应用,用于实时显示BTC价格 ✨ 功能特性: • 实时显示BTC/USDT价格 - • 每30秒自动刷新 + • 可配置刷新间隔(当前:\(currentInterval.displayText)) • 支持手动刷新 (Cmd+R) • 智能错误重试机制 • 优雅的SF Symbols图标 - - 📊 技术信息: - 数据来源:币安官方API - 作者:张雷 - 版本:1.0.0 - 架构:SwiftUI + AppKit """ alert.alertStyle = .informational alert.addButton(withTitle: "确定") alert.runModal() } + // 打开GitHub页面 + @objc private func checkForUpdates() { + let githubURL = "https://github.com/jiayouzl/Bitcoin-Monitoring" + + // 确保URL有效 + guard let url = URL(string: githubURL) else { + print("❌ 无效的URL: \(githubURL)") + return + } + + // 使用默认浏览器打开URL + NSWorkspace.shared.open(url) + + print("✅ 已在浏览器中打开GitHub页面: \(githubURL)") + } + + // 获取应用版本信息 + /// - Returns: 版本号字符串,格式为 "主版本号.次版本号.修订号" + private func getAppVersion() -> String { + guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { + return "未知版本" + } + + return version + } + // 退出应用 @objc private func quitApp() { NSApplication.shared.terminate(nil) diff --git a/test1/BTCPriceResponse.swift b/test1/BTCPriceResponse.swift index d017c4d..d8273d7 100644 --- a/test1/BTCPriceResponse.swift +++ b/test1/BTCPriceResponse.swift @@ -2,7 +2,7 @@ // BTCPriceResponse.swift // test1 // -// Created by zl_vm on 2025/10/28. +// Created by Mark on 2025/10/28. // import Foundation @@ -11,4 +11,4 @@ import Foundation struct BTCPriceResponse: Codable { let symbol: String let price: String -} \ No newline at end of file +} diff --git a/test1/ContentView.swift b/test1/ContentView.swift deleted file mode 100644 index 3b33891..0000000 --- a/test1/ContentView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ContentView.swift -// test1 -// -// Created by zl_vm on 2025/10/28. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") - } - .padding() - } -} - -#Preview { - ContentView() -} diff --git a/test1/PriceManager.swift b/test1/PriceManager.swift index 7099d23..3cffd18 100644 --- a/test1/PriceManager.swift +++ b/test1/PriceManager.swift @@ -2,7 +2,7 @@ // PriceManager.swift // test1 // -// Created by zl_vm on 2025/10/28. +// Created by Mark on 2025/10/28. // import Foundation @@ -17,7 +17,7 @@ class PriceManager: ObservableObject { private let priceService = PriceService() private var timer: Timer? - private let refreshInterval: TimeInterval = 30.0 // 30秒刷新一次 + private var currentRefreshInterval: TimeInterval = 30.0 // 当前刷新间隔 init() { startPriceUpdates() @@ -31,28 +31,48 @@ class PriceManager: ObservableObject { // 开始定时更新价格 func startPriceUpdates() { + #if DEBUG + print("⏰ [BTC Price Manager] 启动定时器,刷新间隔: \(Int(currentRefreshInterval))秒") + #endif + // 立即获取一次价格 Task { await fetchPrice() } // 设置定时器,使用weak self避免循环引用 - timer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { [weak self] _ in + timer = Timer.scheduledTimer(withTimeInterval: currentRefreshInterval, repeats: true) { [weak self] _ in Task { @MainActor in await self?.fetchPrice() } } + + #if DEBUG + print("✅ [BTC Price Manager] 定时器启动成功") + #endif } // 停止价格更新 @MainActor func stopPriceUpdates() { + #if DEBUG + print("⏹️ [BTC Price Manager] 停止定时器") + #endif + timer?.invalidate() timer = nil + + #if DEBUG + print("✅ [BTC Price Manager] 定时器已停止") + #endif } // 手动刷新价格 func refreshPrice() async { + #if DEBUG + print("🔄 [BTC Price Manager] 用户手动刷新价格") + #endif + await fetchPrice() } @@ -61,15 +81,35 @@ class PriceManager: ObservableObject { isFetching = true lastError = nil + #if DEBUG + print("🔄 [BTC Price Manager] 开始获取价格...") + #endif + // 重试最多3次 let maxRetries = 3 for attempt in 1...maxRetries { + #if DEBUG + print("📡 [BTC Price Manager] 尝试获取价格 (第\(attempt)次)") + #endif + do { let price = try await priceService.fetchBTCPrice() currentPrice = price + + #if DEBUG + let formatter = DateFormatter() + formatter.timeStyle = .medium + let currentTime = formatter.string(from: Date()) + print("✅ [BTC Price Manager] 价格更新成功: $\(String(format: "%.2f", price)) | 时间: \(currentTime)") + #endif + break // 成功获取价格,退出重试循环 } catch let error as PriceError { + #if DEBUG + print("❌ [BTC Price Manager] 价格获取失败 (第\(attempt)次): \(error.localizedDescription)") + #endif + if attempt == maxRetries { lastError = error } else { @@ -77,6 +117,10 @@ class PriceManager: ObservableObject { try? await Task.sleep(nanoseconds: UInt64(attempt * 1_000_000_000)) // 递增延迟 } } catch { + #if DEBUG + print("❌ [BTC Price Manager] 网络错误 (第\(attempt)次): \(error.localizedDescription)") + #endif + if attempt == maxRetries { lastError = .networkError(error) } else { @@ -86,6 +130,14 @@ class PriceManager: ObservableObject { } isFetching = false + + #if DEBUG + if let error = lastError { + print("⚠️ [BTC Price Manager] 价格获取流程结束,最终失败: \(error.localizedDescription)") + } else { + print("✅ [BTC Price Manager] 价格获取流程结束,成功") + } + #endif } // 格式化价格显示 @@ -109,4 +161,34 @@ class PriceManager: ObservableObject { var errorMessage: String? { return lastError?.localizedDescription } -} \ No newline at end of file + + // MARK: - Refresh Interval Configuration + + /// 更新刷新间隔 + /// - Parameter interval: 新的刷新间隔 + func updateRefreshInterval(_ interval: RefreshInterval) { + let oldInterval = RefreshInterval.allCases.first { $0.rawValue == currentRefreshInterval }?.displayText ?? "未知" + + #if DEBUG + print("⏱️ [BTC Price Manager] 刷新间隔变更: \(oldInterval) → \(interval.displayText)") + #endif + + currentRefreshInterval = interval.rawValue + + // 如果定时器正在运行,重启它以应用新的间隔 + if timer != nil { + #if DEBUG + print("🔄 [BTC Price Manager] 重启定时器以应用新的刷新间隔") + #endif + + stopPriceUpdates() + startPriceUpdates() + } + } + + /// 获取当前刷新间隔 + /// - Returns: 当前的RefreshInterval枚举值 + func getCurrentRefreshInterval() -> RefreshInterval { + return RefreshInterval.allCases.first { $0.rawValue == currentRefreshInterval } ?? .thirtySeconds + } +} diff --git a/test1/PriceService.swift b/test1/PriceService.swift index 86a2084..92f107e 100644 --- a/test1/PriceService.swift +++ b/test1/PriceService.swift @@ -2,7 +2,7 @@ // PriceService.swift // test1 // -// Created by zl_vm on 2025/10/28. +// Created by Mark on 2025/10/28. // import Foundation @@ -66,4 +66,4 @@ enum PriceError: Error, LocalizedError { return "网络错误:\(error.localizedDescription)" } } -} \ No newline at end of file +} diff --git a/test1/RefreshInterval.swift b/test1/RefreshInterval.swift new file mode 100644 index 0000000..5b77a82 --- /dev/null +++ b/test1/RefreshInterval.swift @@ -0,0 +1,39 @@ +// +// RefreshInterval.swift +// Bitcoin Monitoring +// +// Created by Mark on 2025/10/29. +// + +import Foundation + +/// 刷新间隔选项枚举 +/// 定义用户可以选择的价格刷新间隔 +enum RefreshInterval: Double, CaseIterable, Codable { + case fiveSeconds = 5 + case tenSeconds = 10 + case thirtySeconds = 30 + case sixtySeconds = 60 + + /// 获取刷新间隔的显示文本 + /// - Returns: 用于在菜单中显示的中文文本 + var displayText: String { + switch self { + case .fiveSeconds: + return "5秒" + case .tenSeconds: + return "10秒" + case .thirtySeconds: + return "30秒" + case .sixtySeconds: + return "60秒" + } + } + + /// 获取包含当前标记的显示文本 + /// - Parameter isCurrent: 是否为当前选中的间隔 + /// - Returns: 带有当前标记的显示文本 + func displayTextWithMark(isCurrent: Bool) -> String { + return isCurrent ? "✓ \(displayText)" : " \(displayText)" + } +} diff --git a/test1/test1.entitlements b/test1/test1.entitlements index 4e584a0..b83d3b8 100644 --- a/test1/test1.entitlements +++ b/test1/test1.entitlements @@ -2,12 +2,6 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - com.apple.security.network.client - com.apple.security.network.server diff --git a/test1/test1App.swift b/test1/test1App.swift index 1c09bd6..c854ebd 100644 --- a/test1/test1App.swift +++ b/test1/test1App.swift @@ -2,7 +2,7 @@ // test1App.swift // test1 // -// Created by zl_vm on 2025/10/28. +// Created by Mark on 2025/10/28. // import SwiftUI