Files
Bitcoin-Monitoring/Bitcoin-Monitoring/Views/PreferencesWindowView.swift

1013 lines
34 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// PreferencesWindowView.swift
// Bitcoin Monitoring
//
// Created by Mark on 2025/10/31.
//
import SwiftUI
/**
*
*
*/
enum SettingsTab: String, CaseIterable {
case general = "通用"
case custom = "自定义币种"
case proxy = "代理设置"
/// SF Symbols
var icon: String {
switch self {
case .general:
return "gear"
case .custom:
return "plus.circle"
case .proxy:
return "network"
}
}
///
var displayText: String {
return self.rawValue
}
}
/**
*
* 使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 tempProxyUsername: String
@State private var tempProxyPassword: String
@State private var tempLaunchAtLogin: Bool
@State private var tempOptionClickAction: OptionClickAction
//
@State private var showingValidationError = false
@State private var validationErrorMessage = ""
//
@State private var isTestingProxy = false
@State private var showingProxyTestResult = false
@State private var proxyTestResultMessage = ""
@State private var proxyTestSucceeded = false
//
@State private var isSaving = false
//
@State private var customSymbolInput: String = ""
@State private var isCustomSymbolValid: Bool = false
@State private var customSymbolErrorMessage: String?
@State private var showingCustomSymbolDeleteConfirmation: Bool = false
@State private var pendingDeleteIndex: Int? = nil
//
@State private var isValidatingCustomSymbol: Bool = false
@State private var showingValidationFailureAlert: Bool = false
@State private var validationFailureMessage: String = ""
// PriceService
private let priceService: PriceService
// -
@State private var selectedTab: SettingsTab = .general
//
@State private var hoveredTab: SettingsTab? = nil
init(appSettings: AppSettings, onClose: @escaping () -> Void) {
self.appSettings = appSettings
self.priceService = PriceService(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._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 {
mainContentView
.frame(width: 480, height: 500)
.alert("配置验证", isPresented: $showingValidationError) {
Button("确定", role: .cancel) { }
} message: {
Text(validationErrorMessage)
}
.alert("代理测试结果", isPresented: $showingProxyTestResult) {
Button("确定", role: .cancel) { }
} message: {
proxyTestAlertContent
}
.alert("删除自定义币种", isPresented: $showingCustomSymbolDeleteConfirmation) {
Button("取消", role: .cancel) { }
Button("删除", role: .destructive) {
deleteCustomSymbol()
}
} message: {
deleteCustomSymbolMessage
}
.alert("币种验证失败", isPresented: $showingValidationFailureAlert) {
Button("确定", role: .cancel) { }
} message: {
Text(validationFailureMessage)
}
}
//
private var mainContentView: some View {
VStack(spacing: 0) {
//
topTabBarView
Divider()
//
ScrollView {
settingsContentView
.padding(24)
}
Divider()
bottomButtonsView
}
}
//
private var topTabBarView: some View {
HStack(spacing: 0) {
ForEach(SettingsTab.allCases, id: \.self) { tab in
// 使
HStack(spacing: 8) {
Image(systemName: tab.icon)
.font(.system(size: 14))
.foregroundColor(selectedTab == tab ? .blue : .secondary)
Text(tab.displayText)
.font(.system(size: 13))
.fontWeight(selectedTab == tab ? .medium : .regular)
.foregroundColor(selectedTab == tab ? .blue : .primary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity) //
.contentShape(Rectangle()) //
.background(
RoundedRectangle(cornerRadius: 0)
.fill(selectedTab == tab ? Color(NSColor.controlAccentColor).opacity(0.1) : Color.clear)
)
.background(
//
RoundedRectangle(cornerRadius: 0)
.fill(hoveredTab == tab && selectedTab != tab ? Color(NSColor.controlAccentColor).opacity(0.05) : Color.clear)
)
.onTapGesture {
withAnimation(.easeInOut(duration: 0.2)) {
selectedTab = tab
}
}
.onHover { isHovered in
if isHovered {
NSCursor.pointingHand.set()
hoveredTab = tab
} else {
NSCursor.arrow.set()
if hoveredTab == tab {
hoveredTab = nil
}
}
}
// 线
if tab != SettingsTab.allCases.last {
Rectangle()
.fill(Color(NSColor.separatorColor))
.frame(width: 1)
.padding(.vertical, 8)
}
}
}
.frame(height: 44)
.background(Color(NSColor.controlBackgroundColor))
}
// -
private var settingsContentView: some View {
VStack(spacing: 24) {
//
Group {
switch selectedTab {
case .general:
generalSettingsView
case .custom:
customCryptoSettingsView
case .proxy:
proxySettingsView
}
}
Spacer(minLength: 20)
}
}
// + + Option+
private var generalSettingsView: some View {
VStack(spacing: 24) {
refreshSettingsView
launchSettingsView
optionClickSettingsView
}
}
//
private var refreshSettingsView: some View {
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 }
)
}
}
}
}
}
//
private var launchSettingsView: some View {
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()
.toggleStyle(.switch)
.controlSize(.mini)
}
}
}
}
// 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") {
VStack(alignment: .leading, spacing: 16) {
proxyToggleView
proxyConfigView
}
.opacity(tempProxyEnabled ? 1.0 : 0.6)
}
}
//
private var proxyToggleView: some View {
HStack {
Text("启用HTTP代理")
.font(.subheadline)
.foregroundColor(.primary)
Spacer()
Toggle("", isOn: $tempProxyEnabled)
.labelsHidden()
.toggleStyle(.switch)
.controlSize(.mini)
}
}
//
private var proxyConfigView: some View {
VStack(alignment: .leading, spacing: 12) {
Text("代理服务器配置")
.font(.caption)
.foregroundColor(.secondary)
proxyServerConfigView
proxyAuthConfigView
proxyTestButtonView
}
}
//
private var proxyServerConfigView: some View {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("服务器地址")
.font(.caption)
.foregroundColor(.secondary)
TextField("ip or proxy.example.com", text: $tempProxyHost)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(maxWidth: .infinity)
.disabled(!tempProxyEnabled)
}
VStack(alignment: .leading, spacing: 4) {
Text("端口")
.font(.caption)
.foregroundColor(.secondary)
TextField("3128", text: $tempProxyPort)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 80)
.disabled(!tempProxyEnabled)
}
}
}
//
private var proxyAuthConfigView: some View {
VStack(alignment: .leading, spacing: 8) {
Text("认证设置 (可选)")
.font(.caption)
.foregroundColor(.secondary)
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("用户名")
.font(.caption)
.foregroundColor(.secondary)
TextField("user", text: $tempProxyUsername)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(maxWidth: .infinity)
.disabled(!tempProxyEnabled)
}
VStack(alignment: .leading, spacing: 4) {
Text("密码")
.font(.caption)
.foregroundColor(.secondary)
SecureField("password", text: $tempProxyPassword)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(maxWidth: .infinity)
.disabled(!tempProxyEnabled)
}
}
}
}
//
private var proxyTestButtonView: some View {
HStack {
Spacer()
Button(action: testProxyConnection) {
HStack {
if isTestingProxy {
ProgressView()
.scaleEffect(0.4)
.frame(width: 8, height: 8)
} else {
Image(systemName: "network")
.font(.system(size: 12))
}
Text(isTestingProxy ? "测试中..." : "测试连接")
}
.frame(minWidth: 80)
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(!tempProxyEnabled || isTestingProxy || isSaving)
}
}
//
private var customCryptoSettingsView: some View {
SettingsGroupView(title: "自定义币种", icon: "plus.circle") {
VStack(alignment: .leading, spacing: 16) {
//
if !appSettings.customCryptoSymbols.isEmpty {
customSymbolsListView
}
//
addCustomSymbolView
}
}
}
//
private var customSymbolsListView: some View {
VStack(alignment: .leading, spacing: 12) {
Text("已添加的自定义币种 (\(appSettings.customCryptoSymbols.count)/5)")
.font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.primary)
VStack(spacing: 8) {
ForEach(0..<appSettings.customCryptoSymbols.count, id: \.self) { index in
customSymbolRowView(at: index)
}
}
}
}
//
private func customSymbolRowView(at index: Int) -> some View {
let customSymbol = appSettings.customCryptoSymbols[index]
let isSelected = appSettings.isUsingCustomSymbol() && appSettings.selectedCustomSymbolIndex == index
return HStack {
//
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.font(.system(size: 14))
.foregroundColor(isSelected ? .blue : .secondary)
// 使
Group {
let nsImage = customSymbol.customIcon()
Image(nsImage: nsImage)
.resizable()
.aspectRatio(contentMode: .fit)
}
.foregroundColor(.orange)
.font(.system(size: 16))
.frame(width: 16, height: 16)
//
VStack(alignment: .leading, spacing: 2) {
Text(customSymbol.displayName)
.font(.subheadline)
.fontWeight(isSelected ? .medium : .regular)
.foregroundColor(.primary)
Text(customSymbol.pairDisplayName)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
//
Button(action: {
showingCustomSymbolDeleteConfirmation = true
pendingDeleteIndex = index
}) {
Image(systemName: "trash")
.font(.system(size: 12))
.foregroundColor(.red)
}
.buttonStyle(PlainButtonStyle())
.frame(width: 24, height: 24)
.background(
Circle()
.fill(Color.red.opacity(0.1))
)
.onHover { isHovered in
if isHovered {
NSCursor.pointingHand.set()
} else {
NSCursor.arrow.set()
}
}
.help("删除")
}
.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)
)
.contentShape(RoundedRectangle(cornerRadius: 6))
.onTapGesture {
//
appSettings.selectCustomCryptoSymbol(at: index)
}
}
//
private var addCustomSymbolView: some View {
VStack(alignment: .leading, spacing: 12) {
Text(appSettings.customCryptoSymbols.isEmpty ? "添加自定义币种" : "添加更多自定义币种")
.font(.subheadline)
.foregroundColor(.primary)
Text("输入3-5个大写字母的币种符号如 ENA、TRX、TRUMP")
.font(.caption)
.foregroundColor(.secondary)
//
if appSettings.customCryptoSymbols.count >= 5 {
Text("已达到最大限制5个币种")
.font(.caption)
.foregroundColor(.orange)
}
customSymbolInputView
}
}
//
private var customSymbolInputView: some View {
VStack(alignment: .leading, spacing: 8) {
Text("币种符号")
.font(.caption)
.foregroundColor(.secondary)
HStack(spacing: 12) {
TextField("例如: TRX", text: Binding(
get: { customSymbolInput },
set: { newValue in
let filteredValue = newValue.filter { $0.isLetter }.uppercased()
customSymbolInput = String(filteredValue.prefix(5))
let validation = CustomCryptoSymbol.isValidSymbol(customSymbolInput)
isCustomSymbolValid = validation.isValid
customSymbolErrorMessage = validation.errorMessage
}
))
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(maxWidth: .infinity)
.onSubmit {
//
Task {
await addCustomSymbolWithValidation()
}
}
Button {
Task {
await addCustomSymbolWithValidation()
}
} label: {
if isValidatingCustomSymbol {
HStack(spacing: 6) {
ProgressView()
.controlSize(.small)
Text("验证中...")
.font(.system(size: 13, weight: .medium))
}
} else {
Text("添加")
.font(.system(size: 14, weight: .medium))
}
}
.buttonStyle(.borderedProminent)
.controlSize(.regular)
.frame(width: 70, height: 32)
.disabled(!isCustomSymbolValid || isSaving || isValidatingCustomSymbol || appSettings.customCryptoSymbols.count >= 5)
}
if !isCustomSymbolValid && !customSymbolInput.isEmpty {
Text(customSymbolErrorMessage ?? "输入格式不正确")
.font(.caption)
.foregroundColor(.red)
.padding(.leading, 4)
}
if customSymbolInput.isEmpty {
Text("输入币种符号后将自动验证")
.font(.caption)
.foregroundColor(.gray)
.padding(.leading, 4)
}
}
}
//
private var bottomButtonsView: some View {
HStack {
Spacer()
Button("取消") {
onClose()
}
.keyboardShortcut(.escape)
Button(action: saveSettings) {
HStack {
if isSaving {
ProgressView()
.scaleEffect(0.4)
.frame(width: 8, height: 8)
}
Text("保存")
}
.frame(minWidth: 80)
}
.buttonStyle(.borderedProminent)
.keyboardShortcut(.defaultAction)
.disabled(isSaving)
}
.padding(.horizontal, 24)
.padding(.vertical, 16)
}
//
private var proxyTestAlertContent: some View {
HStack {
Image(systemName: proxyTestSucceeded ? "checkmark.circle.fill" : "xmark.circle.fill")
.foregroundColor(proxyTestSucceeded ? .green : .red)
Text(proxyTestResultMessage)
}
}
//
private var deleteCustomSymbolMessage: Text {
if let index = pendingDeleteIndex,
index >= 0 && index < appSettings.customCryptoSymbols.count {
let customSymbol = appSettings.customCryptoSymbols[index]
return Text("确定要删除自定义币种 \"\(customSymbol.displayName)\" 吗?删除后将无法恢复。")
} else {
return Text("确定要删除自定义币种吗?删除后将无法恢复。")
}
}
/**
*
*/
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)")
}
// Option+
if tempOptionClickAction != appSettings.optionClickAction {
appSettings.saveOptionClickAction(tempOptionClickAction)
print("✅ [Preferences] 已保存Option+点击功能: \(tempOptionClickAction.displayName)")
}
//
let port = Int(tempProxyPort) ?? 3128
appSettings.saveProxySettings(
enabled: tempProxyEnabled,
host: tempProxyHost,
port: port,
username: tempProxyUsername,
password: tempProxyPassword
)
if tempProxyEnabled {
let authInfo = !tempProxyUsername.isEmpty ? " (认证: \(tempProxyUsername))" : ""
print("✅ [Preferences] 已保存代理设置: \(tempProxyHost):\(port)\(authInfo)")
} else {
print("✅ [Preferences] 已禁用代理设置")
}
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isSaving = false
onClose()
}
}
/**
*
*/
private func testProxyConnection() {
print("🔧 [Preferences] 开始测试代理连接...")
//
let validation = validateProxyInput()
if !validation.isValid {
proxyTestResultMessage = validation.errorMessage ?? "配置验证失败"
proxyTestSucceeded = false
showingProxyTestResult = true
return
}
isTestingProxy = true
Task {
//
let tempAppSettings = AppSettings()
tempAppSettings.saveProxySettings(
enabled: true,
host: tempProxyHost.trimmingCharacters(in: .whitespacesAndNewlines),
port: Int(tempProxyPort) ?? 3128,
username: tempProxyUsername.trimmingCharacters(in: .whitespacesAndNewlines),
password: tempProxyPassword
)
let tempPriceService = PriceService(appSettings: tempAppSettings)
let success = await tempPriceService.testProxyConnection()
await MainActor.run {
isTestingProxy = false
if success {
proxyTestResultMessage = "代理连接测试成功可以正常访问币安API。"
proxyTestSucceeded = true
print("✅ [Preferences] 代理连接测试成功")
} else {
proxyTestResultMessage = "代理连接测试失败,请检查代理配置或网络连接。"
proxyTestSucceeded = false
print("❌ [Preferences] 代理连接测试失败")
}
showingProxyTestResult = true
}
}
}
/**
*
* - 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)
}
// MARK: -
/**
* API
*/
private func addCustomSymbolWithValidation() async {
guard isCustomSymbolValid, !customSymbolInput.isEmpty else {
return
}
do {
let customSymbol = try CustomCryptoSymbol(symbol: customSymbolInput)
//
isValidatingCustomSymbol = true
// API
let isValid = await priceService.validateCustomSymbol(customSymbol.symbol)
await MainActor.run {
isValidatingCustomSymbol = false
if isValid {
//
let success = appSettings.addCustomCryptoSymbol(customSymbol)
if success {
//
customSymbolInput = ""
isCustomSymbolValid = false
customSymbolErrorMessage = nil
print("✅ [Preferences] 已添加自定义币种: \(customSymbol.displayName)")
} else {
//
customSymbolErrorMessage = "无法添加该币种(可能已达到最大限制或币种重复)"
isCustomSymbolValid = false
}
} else {
//
validationFailureMessage = "币种 \"\(customSymbol.symbol)\" 在币安交易所中不存在,请检查币种代码是否正确"
showingValidationFailureAlert = true
isCustomSymbolValid = false
customSymbolErrorMessage = "币种不存在或无法获取价格"
}
}
} catch {
await MainActor.run {
isValidatingCustomSymbol = false
// onChange
print("❌ [Preferences] 添加自定义币种失败: \(error.localizedDescription)")
customSymbolErrorMessage = "添加失败:\(error.localizedDescription)"
isCustomSymbolValid = false
}
}
}
/**
*
*/
private func addCustomSymbol() {
guard isCustomSymbolValid, !customSymbolInput.isEmpty else {
return
}
do {
let customSymbol = try CustomCryptoSymbol(symbol: customSymbolInput)
// 使
let success = appSettings.addCustomCryptoSymbol(customSymbol)
if success {
//
customSymbolInput = ""
isCustomSymbolValid = false
customSymbolErrorMessage = nil
print("✅ [Preferences] 已添加自定义币种: \(customSymbol.displayName)")
} else {
//
customSymbolErrorMessage = "无法添加该币种(可能已达到最大限制或币种重复)"
isCustomSymbolValid = false
}
} catch {
// onChange
print("❌ [Preferences] 添加自定义币种失败: \(error.localizedDescription)")
customSymbolErrorMessage = "添加失败:\(error.localizedDescription)"
isCustomSymbolValid = false
}
}
/**
*
*/
private func deleteCustomSymbol() {
guard let index = pendingDeleteIndex else {
print("❌ [Preferences] 删除失败:无效的索引")
return
}
appSettings.removeCustomCryptoSymbol(at: index)
pendingDeleteIndex = nil
print("✅ [Preferences] 已删除自定义币种")
}
}
/**
*
*/
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 {
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)
)
.contentShape(RoundedRectangle(cornerRadius: 6)) //
.onTapGesture {
onSelect()
}
}
}
#Preview {
PreferencesWindowView(
appSettings: AppSettings(),
onClose: {}
)
}