From 0bfb6d25160be9a324b5161efaa5673623c95e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 26 Nov 2025 11:12:33 +0800 Subject: [PATCH] Add swiftlint & Minor fixes --- .swiftlint.yml | 21 ++++ .../Abstract/NavigationButtonsView.swift | 28 +++--- .../Views/Connections/Connection.swift | 13 ++- .../Connections/ConnectionListView.swift | 14 ++- .../Connections/ConnectionListViewModel.swift | 20 +--- .../Views/Dashboard/ActiveDashboardView.swift | 83 ++++++++++------ .../Dashboard/Cards/CardManagementSheet.swift | 17 +--- .../Cards/DashboardCardConfiguration.swift | 8 +- .../Views/Dashboard/Cards/HTTPProxyCard.swift | 15 ++- .../Views/Dashboard/Cards/ProfileCard.swift | 27 +++--- .../Cards/ProfileSelectorButton.swift | 6 +- .../Components/StartStopButton.swift | 95 ++++++++++++++----- .../Views/Dashboard/DashboardView.swift | 17 +++- .../Views/Groups/GroupListView.swift | 4 +- .../Views/Groups/GroupListViewModel.swift | 17 +--- .../Views/Groups/OutboundGroup.swift | 15 +-- ApplicationLibrary/Views/Log/LogView.swift | 6 +- .../Views/Profile/ProfileActionToolbar.swift | 33 ++++++- .../Views/Profile/ProfileView.swift | 8 +- .../Views/Profile/QRCodeSheet.swift | 8 -- .../Views/Setting/CoreView.swift | 1 - .../Views/Setting/MacAppView.swift | 1 - .../Views/Setting/SettingView.swift | 1 - Localizable.xcstrings | 77 +++++++++++++-- MacLibrary/ApplicationDelegate.swift | 2 - MacLibrary/MainView.swift | 10 +- Makefile | 25 +++++ SFI/ApplicationDelegate.swift | 2 - SFI/MainView.swift | 4 +- 29 files changed, 382 insertions(+), 196 deletions(-) create mode 100644 .swiftlint.yml create mode 100644 Makefile diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..806730c --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,21 @@ +excluded: + - Frameworks + - Libbox.xcframework + - build + +disabled_rules: + - identifier_name + - type_body_length + - file_length + - function_body_length + - cyclomatic_complexity + - large_tuple + - nesting + - line_length + - todo + - trailing_comma + - function_parameter_count + - type_name + - force_cast + - force_try + - opening_brace diff --git a/ApplicationLibrary/Views/Abstract/NavigationButtonsView.swift b/ApplicationLibrary/Views/Abstract/NavigationButtonsView.swift index 609d5d1..9ec2edd 100644 --- a/ApplicationLibrary/Views/Abstract/NavigationButtonsView.swift +++ b/ApplicationLibrary/Views/Abstract/NavigationButtonsView.swift @@ -27,20 +27,6 @@ public struct NavigationButtonsView: View { public var body: some View { HStack(spacing: 12) { - if showGroupsButton { - Divider() - Text(verbatim: "\(groupsCount)") - .font(.subheadline) - .foregroundStyle(.secondary) - .fixedSize() - Button { - onGroupsTap() - } label: { - Label("Groups", systemImage: "rectangle.3.group.fill") - } - .labelStyle(.iconOnly) - .foregroundStyle(.primary) - } if showConnectionsButton { Divider() Text(verbatim: "\(connectionsCount)") @@ -55,6 +41,20 @@ public struct NavigationButtonsView: View { .labelStyle(.iconOnly) .foregroundStyle(.primary) } + if showGroupsButton { + Divider() + Text(verbatim: "\(groupsCount)") + .font(.subheadline) + .foregroundStyle(.secondary) + .fixedSize() + Button { + onGroupsTap() + } label: { + Label("Groups", systemImage: "rectangle.3.group.fill") + } + .labelStyle(.iconOnly) + .foregroundStyle(.primary) + } } } } diff --git a/ApplicationLibrary/Views/Connections/Connection.swift b/ApplicationLibrary/Views/Connections/Connection.swift index 2047597..1238adf 100644 --- a/ApplicationLibrary/Views/Connections/Connection.swift +++ b/ApplicationLibrary/Views/Connections/Connection.swift @@ -24,13 +24,12 @@ public struct Connection: Codable { public let outboundType: String public let chain: [String] - var hashValue: Int { - var value = id.hashValue - (value, _) = value.addingReportingOverflow(upload.hashValue) - (value, _) = value.addingReportingOverflow(download.hashValue) - (value, _) = value.addingReportingOverflow(uploadTotal.hashValue) - (value, _) = value.addingReportingOverflow(downloadTotal.hashValue) - return value + func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(upload) + hasher.combine(download) + hasher.combine(uploadTotal) + hasher.combine(downloadTotal) } func performSearch(_ content: String) -> Bool { diff --git a/ApplicationLibrary/Views/Connections/ConnectionListView.swift b/ApplicationLibrary/Views/Connections/ConnectionListView.swift index 6439471..56c8940 100644 --- a/ApplicationLibrary/Views/Connections/ConnectionListView.swift +++ b/ApplicationLibrary/Views/Connections/ConnectionListView.swift @@ -17,7 +17,7 @@ public struct ConnectionListView: View { } else { ScrollView { LazyVGrid(columns: [GridItem(.flexible())], alignment: .leading) { - ForEach(viewModel.filteredConnections(), id: \.hashValue) { it in + ForEach(viewModel.filteredConnections(), id: \.id) { it in ConnectionView(it) } } @@ -56,9 +56,19 @@ public struct ConnectionListView: View { #endif .alertBinding($viewModel.alert) .onAppear { - viewModel.setCommandClient(environments.commandClient) viewModel.connect() } + .onReceive(environments.commandClient.$connections) { connections in + viewModel.setConnections(connections) + } + .onChangeCompat(of: viewModel.connectionStateFilter) { filter in + environments.commandClient.connectionStateFilter = filter + environments.commandClient.filterConnectionsNow() + } + .onChangeCompat(of: viewModel.connectionSort) { sort in + environments.commandClient.connectionSort = sort + environments.commandClient.filterConnectionsNow() + } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) #if os(iOS) .background(Color(uiColor: .systemGroupedBackground)) diff --git a/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift b/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift index 8fe043d..aadc29c 100644 --- a/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift +++ b/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift @@ -1,4 +1,3 @@ -import Combine import Libbox import Library import SwiftUI @@ -11,8 +10,6 @@ public class ConnectionListViewModel: ObservableObject { @Published public var alert: Alert? @Published public var connectionStateFilter: ConnectionStateFilter { didSet { - commandClient?.connectionStateFilter = connectionStateFilter - commandClient?.filterConnectionsNow() saveStateFilterTask?.cancel() saveStateFilterTask = Task { await SharedPreferences.connectionStateFilter.set(connectionStateFilter.rawValue) @@ -22,8 +19,6 @@ public class ConnectionListViewModel: ObservableObject { @Published public var connectionSort: ConnectionSort { didSet { - commandClient?.connectionSort = connectionSort - commandClient?.filterConnectionsNow() saveSortTask?.cancel() saveSortTask = Task { await SharedPreferences.connectionSort.set(connectionSort.rawValue) @@ -31,8 +26,6 @@ public class ConnectionListViewModel: ObservableObject { } } - private var commandClient: CommandClient? - private var cancellables = Set() private var connectTask: Task? private var saveStateFilterTask: Task? private var saveSortTask: Task? @@ -42,16 +35,6 @@ public class ConnectionListViewModel: ObservableObject { connectionSort = .byDate } - public func setCommandClient(_ client: CommandClient) { - commandClient = client - client.$connections - .compactMap { $0 } - .sink { [weak self] goConnections in - self?.setConnections(goConnections) - } - .store(in: &cancellables) - } - public func connect() { if ApplicationLibrary.inPreview { isLoading = false @@ -97,7 +80,8 @@ public class ConnectionListViewModel: ObservableObject { } } - private func setConnections(_ goConnections: [LibboxConnection]) { + public func setConnections(_ goConnections: [LibboxConnection]?) { + guard let goConnections else { return } connections = convertConnections(goConnections) isLoading = false } diff --git a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift index 805d9e1..8df3e7e 100644 --- a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift @@ -111,22 +111,25 @@ public struct ActiveDashboardView: View { #if os(iOS) || os(tvOS) private func updateButtonVisibility() { - buttonState.update(profile: profile, commandClient: environments.commandClient) + buttonState.update(profile: profile, commandClient: environments.commandClient, requireAnyConnection: true) } + #if os(iOS) + private var isTabViewBottomAccessoryAvailable: Bool { + if #available(iOS 26.0, *), !Variant.debugNoIOS26 { + return true + } + return false + } + #endif + @ToolbarContentBuilder private var toolbar: some ToolbarContent { - ToolbarItem(placement: .topBarLeading) { - #if os(iOS) - if #available(iOS 26.0, *), !Variant.debugNoIOS26 { - EmptyView() - } else { - navigationButtons - } - #else + #if os(tvOS) + ToolbarItem(placement: .topBarLeading) { navigationButtons - #endif - } + } + #endif ToolbarItem(placement: .topBarTrailing) { if #available(iOS 16.0, tvOS 17.0, *) { cardManagementButton @@ -137,10 +140,7 @@ public struct ActiveDashboardView: View { if #available(iOS 26.0, *), !Variant.debugNoIOS26 { EmptyView() } else { - HStack(spacing: 12) { - Divider() - StartStopButton() - } + StartStopButton() } #else HStack(spacing: 12) { @@ -151,16 +151,18 @@ public struct ActiveDashboardView: View { } } - private var navigationButtons: some View { - NavigationButtonsView( - showGroupsButton: buttonState.showGroupsButton, - showConnectionsButton: buttonState.showConnectionsButton, - groupsCount: buttonState.groupsCount, - connectionsCount: buttonState.connectionsCount, - onGroupsTap: { showGroups = true }, - onConnectionsTap: { showConnections = true } - ) - } + #if os(tvOS) + private var navigationButtons: some View { + NavigationButtonsView( + showGroupsButton: buttonState.showGroupsButton, + showConnectionsButton: buttonState.showConnectionsButton, + groupsCount: buttonState.groupsCount, + connectionsCount: buttonState.connectionsCount, + onGroupsTap: { showGroups = true }, + onConnectionsTap: { showConnections = true } + ) + } + #endif #endif #if os(iOS) || os(tvOS) @@ -176,19 +178,42 @@ public struct ActiveDashboardView: View { @ViewBuilder private var cardManagementButton: some View { Menu { + #if os(iOS) + if !isTabViewBottomAccessoryAvailable { + if buttonState.showGroupsButton { + Button { + showGroups = true + } label: { + Label("Groups (\(buttonState.groupsCount))", systemImage: "rectangle.3.group.fill") + } + } + if buttonState.showConnectionsButton { + Button { + showConnections = true + } label: { + Label("Connections (\(buttonState.connectionsCount))", systemImage: "list.bullet.rectangle.portrait.fill") + } + } + if buttonState.showGroupsButton || buttonState.showConnectionsButton { + Divider() + } + } + #endif Button { showCardManagement = true } label: { Label("Dashboard Items", systemImage: "square.grid.2x2") } } label: { - Label("Others", systemImage: "ellipsis.circle") + Label("Others", systemImage: "line.3.horizontal.circle") } - .sheet(isPresented: $showCardManagement) { - CardManagementSheet(configurationVersion: $cardConfigurationVersion) + .sheet(isPresented: $showCardManagement, onDismiss: { + cardConfigurationVersion += 1 + }, content: { + CardManagementSheet() .presentationDetents([.large]) .presentationDragIndicator(.visible) - } + }) } #endif } diff --git a/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift b/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift index 983c0d6..7fb7eef 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift @@ -4,11 +4,8 @@ import SwiftUI @MainActor public struct CardManagementSheet: View { @Environment(\.dismiss) private var dismiss @StateObject private var configuration = DashboardCardConfiguration() - @Binding private var configurationVersion: Int - public init(configurationVersion: Binding) { - _configurationVersion = configurationVersion - } + public init() {} public var body: some View { #if os(macOS) @@ -41,7 +38,6 @@ import SwiftUI Button("Reset", role: .destructive) { Task { await configuration.resetToDefault() - configurationVersion += 1 } } } @@ -49,11 +45,9 @@ import SwiftUI Button("Done") { dismiss() } + .keyboardShortcut(.escape, modifiers: []) } } - .onExitCommand { - dismiss() - } } #else private var iOSBody: some View { @@ -74,7 +68,6 @@ import SwiftUI Button("Reset", role: .destructive) { Task { await configuration.resetToDefault() - configurationVersion += 1 } } } @@ -91,13 +84,13 @@ import SwiftUI isEnabled: configuration.isEnabled(card), onToggle: { configuration.toggleCard(card) - configurationVersion += 1 } ) } .onMove { source, destination in - configuration.moveCard(from: source, to: destination) - configurationVersion += 1 + Task { + await configuration.moveCard(from: source, to: destination) + } } } .applyContentMargins() diff --git a/ApplicationLibrary/Views/Dashboard/Cards/DashboardCardConfiguration.swift b/ApplicationLibrary/Views/Dashboard/Cards/DashboardCardConfiguration.swift index 35d28c0..0977fd7 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/DashboardCardConfiguration.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/DashboardCardConfiguration.swift @@ -40,13 +40,9 @@ public final class DashboardCardConfiguration: ObservableObject { } } - public func moveCard(from source: IndexSet, to destination: Int) { + public func moveCard(from source: IndexSet, to destination: Int) async { cardOrder.move(fromOffsets: source, toOffset: destination) - - // Save asynchronously in background - Task { - await saveCardOrder() - } + await saveCardOrder() } public func resetToDefault() async { diff --git a/ApplicationLibrary/Views/Dashboard/Cards/HTTPProxyCard.swift b/ApplicationLibrary/Views/Dashboard/Cards/HTTPProxyCard.swift index e5a0974..68d5b46 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/HTTPProxyCard.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/HTTPProxyCard.swift @@ -19,12 +19,17 @@ public struct HTTPProxyCard: View { public var body: some View { DashboardCardView(title: "", isHalfWidth: false) { - Toggle("System HTTP Proxy", isOn: $systemProxyEnabled) - .onChangeCompat(of: systemProxyEnabled) { newValue in - Task { - await onToggle(newValue) + HStack { + Text("System HTTP Proxy") + Spacer() + Toggle("", isOn: $systemProxyEnabled) + .labelsHidden() + .onChangeCompat(of: systemProxyEnabled) { newValue in + Task { + await onToggle(newValue) + } } - } + } } } } diff --git a/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift b/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift index 73debf7..89c723e 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift @@ -34,10 +34,10 @@ public struct ProfileCard: View { .disabled(viewModel.isUpdating) .sheet(isPresented: $viewModel.showNewProfile, onDismiss: { environments.profileUpdate.send() - }) { + }, content: { NewProfileNavigationView() .environmentObject(environments) - } + }) .sheet(isPresented: $viewModel.showManageProfiles) { manageProfilesSheet } @@ -218,11 +218,12 @@ public struct ProfileCard: View { NavigationSheet( title: String(localized: "Manage profiles"), showDoneButton: true, - onDismiss: { viewModel.showManageProfiles = false } - ) { - ManageProfilesView() - .environmentObject(environments) - } + onDismiss: { viewModel.showManageProfiles = false }, + content: { + ManageProfilesView() + .environmentObject(environments) + } + ) } @ViewBuilder @@ -390,6 +391,7 @@ extension ProfileCard { @ObservedObject private var viewModel: ProfileViewModel @State private var profile: ProfilePreview @State private var shareLinkPresented = false + @State private var isUpdating = false init(_ viewModel: ProfileViewModel, _ profile: ProfilePreview) { self.viewModel = viewModel @@ -416,25 +418,26 @@ extension ProfileCard { HStack(spacing: 8) { if profile.type == .remote { Button { - viewModel.isUpdating = true + isUpdating = true Task { await viewModel.updateProfile(profile.origin) profile = ProfilePreview(profile.origin) + isUpdating = false } } label: { Image(systemName: "arrow.clockwise") .font(.system(size: 16)) - .rotationEffect(.degrees(viewModel.isUpdating ? 360 : 0)) + .rotationEffect(.degrees(isUpdating ? 360 : 0)) .animation( - viewModel.isUpdating + isUpdating ? .linear(duration: 1).repeatForever(autoreverses: false) : .default, - value: viewModel.isUpdating + value: isUpdating ) } .buttonStyle(.plain) .actionButtonStyle() - .disabled(viewModel.isUpdating) + .disabled(isUpdating) Button { shareLinkPresented = true diff --git a/ApplicationLibrary/Views/Dashboard/Cards/ProfileSelectorButton.swift b/ApplicationLibrary/Views/Dashboard/Cards/ProfileSelectorButton.swift index c1b59f0..55cd193 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/ProfileSelectorButton.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/ProfileSelectorButton.swift @@ -83,10 +83,8 @@ struct ProfileSelectorButton: View { private func updateButtonContent(_ button: MenuAttachmentButton) { // Remove existing subviews - for subview in button.subviews { - if subview is UIStackView { - subview.removeFromSuperview() - } + for subview in button.subviews where subview is UIStackView { + subview.removeFromSuperview() } // Create content stack diff --git a/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift b/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift index f5ea34a..71b551f 100644 --- a/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift +++ b/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift @@ -42,35 +42,72 @@ public struct StartStopButton: View { await switchProfile(!profile.status.isConnected) } } label: { - HStack(spacing: 8) { - if profile.status.isConnectedStrict, let duration = runtimeDuration { - Text(duration) - .font(.caption) - .foregroundStyle(.secondary) - .monospacedDigit() - .transition(.asymmetric( - insertion: .move(edge: .trailing).combined(with: .opacity), - removal: .move(edge: .trailing).combined(with: .opacity) - )) - } + #if os(iOS) + HStack(spacing: 8) { + if showRuntimeDuration, profile.status.isConnectedStrict, let duration = runtimeDuration { + Text(duration) + .font(.caption) + .foregroundStyle(.secondary) + .monospacedDigit() + .fixedSize() + .transition(.asymmetric( + insertion: .move(edge: .trailing).combined(with: .opacity), + removal: .move(edge: .trailing).combined(with: .opacity) + )) + } - if !profile.status.isConnected { - Label("Start", systemImage: "play.fill") - } else { - Label("Stop", systemImage: "stop.fill") + if !profile.status.isConnected { + Label("Start", systemImage: "play.fill") + } else { + Label("Stop", systemImage: "stop.fill") + } } - } - .animation(.spring(response: 0.35, dampingFraction: 0.75), value: profile.status.isConnectedStrict) + .animation(.spring(response: 0.35, dampingFraction: 0.75), value: profile.status.isConnectedStrict) + #else + HStack(spacing: 8) { + if profile.status.isConnectedStrict, let duration = runtimeDuration { + Text(duration) + .font(.caption) + .foregroundStyle(.secondary) + .monospacedDigit() + .fixedSize() + .transition(.asymmetric( + insertion: .move(edge: .trailing).combined(with: .opacity), + removal: .move(edge: .trailing).combined(with: .opacity) + )) + } + + if !profile.status.isConnected { + Label("Start", systemImage: "play.fill") + } else { + Label("Stop", systemImage: "stop.fill") + } + } + .animation(.spring(response: 0.35, dampingFraction: 0.75), value: profile.status.isConnectedStrict) + #endif } .labelStyle(.iconOnly) - .tint(.primary) - .disabled(!profile.status.isEnabled) - .alertBinding($alert) - .onReceive(timer) { _ in - currentTime = Date() - } + #if os(iOS) + .modifier(PrimaryTintModifier()) + #else + .tint(.primary) + #endif + .disabled(!profile.status.isEnabled) + .alertBinding($alert) + .onReceive(timer) { _ in + currentTime = Date() + } } + #if os(iOS) + private var showRuntimeDuration: Bool { + if #available(iOS 26.0, *), !Variant.debugNoIOS26 { + return true + } + return false + } + #endif + private var runtimeDuration: String? { guard let connectedDate = profile.connectedDate else { return nil } let interval = currentTime.timeIntervalSince(connectedDate) @@ -103,3 +140,15 @@ public struct StartStopButton: View { } } } + +#if os(iOS) + private struct PrimaryTintModifier: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 26.0, *), !Variant.debugNoIOS26 { + content.tint(.primary) + } else { + content + } + } + } +#endif diff --git a/ApplicationLibrary/Views/Dashboard/DashboardView.swift b/ApplicationLibrary/Views/Dashboard/DashboardView.swift index 7aa7e30..2bd9523 100644 --- a/ApplicationLibrary/Views/Dashboard/DashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/DashboardView.swift @@ -85,10 +85,10 @@ public struct DashboardView: View { private func importRemoteProfileSheet(for request: NewProfileView.ImportRequest) -> some View { NavigationSheet(title: "Import Profile", onDismiss: { environments.profileUpdate.send() - }) { + }, content: { NewProfileView(request) .environmentObject(environments) - } + }) } @ViewBuilder @@ -111,11 +111,11 @@ public struct DashboardView: View { @ViewBuilder private var mainContent: some View { if ApplicationLibrary.inPreview { - ActiveDashboardView(externalCardConfigurationVersion: cardConfigurationVersion) + activeDashboardView } else if environments.extensionProfileLoading { ProgressView() } else if let profile = environments.extensionProfile { - ActiveDashboardView(externalCardConfigurationVersion: cardConfigurationVersion) + activeDashboardView .environmentObject(profile) .alertBinding($coordinator.alert) .onChangeCompat(of: profile.status) { status in @@ -129,4 +129,13 @@ public struct DashboardView: View { } } } + + @ViewBuilder + private var activeDashboardView: some View { + #if os(macOS) + ActiveDashboardView(externalCardConfigurationVersion: cardConfigurationVersion) + #else + ActiveDashboardView() + #endif + } } diff --git a/ApplicationLibrary/Views/Groups/GroupListView.swift b/ApplicationLibrary/Views/Groups/GroupListView.swift index a161ba9..e066490 100644 --- a/ApplicationLibrary/Views/Groups/GroupListView.swift +++ b/ApplicationLibrary/Views/Groups/GroupListView.swift @@ -23,8 +23,10 @@ public struct GroupListView: View { } } .onAppear { - viewModel.setCommandClient(environments.commandClient) viewModel.connect() } + .onReceive(environments.commandClient.$groups) { groups in + viewModel.setGroups(groups) + } } } diff --git a/ApplicationLibrary/Views/Groups/GroupListViewModel.swift b/ApplicationLibrary/Views/Groups/GroupListViewModel.swift index 3a962fe..9a2f133 100644 --- a/ApplicationLibrary/Views/Groups/GroupListViewModel.swift +++ b/ApplicationLibrary/Views/Groups/GroupListViewModel.swift @@ -1,4 +1,3 @@ -import Combine import Libbox import Library import SwiftUI @@ -8,21 +7,8 @@ public class GroupListViewModel: ObservableObject { @Published public var isLoading = true @Published public var groups: [OutboundGroup] = [] - private var commandClient: CommandClient? - private var cancellables = Set() - public init() {} - public func setCommandClient(_ client: CommandClient) { - commandClient = client - client.$groups - .compactMap { $0 } - .sink { [weak self] goGroups in - self?.setGroups(goGroups) - } - .store(in: &cancellables) - } - public func connect() { if ApplicationLibrary.inPreview { groups = [ @@ -40,7 +26,8 @@ public class GroupListViewModel: ObservableObject { } } - private func setGroups(_ goGroups: [LibboxOutboundGroup]) { + public func setGroups(_ goGroups: [LibboxOutboundGroup]?) { + guard let goGroups else { return } var groups = [OutboundGroup]() for goGroup in goGroups { var items = [OutboundGroupItem]() diff --git a/ApplicationLibrary/Views/Groups/OutboundGroup.swift b/ApplicationLibrary/Views/Groups/OutboundGroup.swift index 1e38a39..5ca4245 100644 --- a/ApplicationLibrary/Views/Groups/OutboundGroup.swift +++ b/ApplicationLibrary/Views/Groups/OutboundGroup.swift @@ -2,7 +2,7 @@ import Foundation import Libbox import SwiftUI -public struct OutboundGroup: Codable { +public struct OutboundGroup: Codable, Hashable { let tag: String let type: String var selected: String @@ -10,13 +10,16 @@ public struct OutboundGroup: Codable { var isExpand: Bool let items: [OutboundGroupItem] - var hashValue: Int { - var value = tag.hashValue - (value, _) = value.addingReportingOverflow(selected.hashValue) + public func hash(into hasher: inout Hasher) { + hasher.combine(tag) + hasher.combine(selected) for item in items { - (value, _) = value.addingReportingOverflow(item.urlTestTime.hashValue) + hasher.combine(item.urlTestTime) } - return value + } + + public static func == (lhs: OutboundGroup, rhs: OutboundGroup) -> Bool { + lhs.hashValue == rhs.hashValue } } diff --git a/ApplicationLibrary/Views/Log/LogView.swift b/ApplicationLibrary/Views/Log/LogView.swift index e676518..9491a3e 100644 --- a/ApplicationLibrary/Views/Log/LogView.swift +++ b/ApplicationLibrary/Views/Log/LogView.swift @@ -85,7 +85,7 @@ private struct LogViewContent: View { .focusEffectDisabled() .focusSection() #else - let previewLogs = logList.enumerated().map { _, message in + let previewLogs = logList.map { message in LogEntry(level: 4, message: message) } return LogTextView( @@ -205,9 +205,9 @@ private struct LogViewContent: View { Button(action: { viewModel.prepareLogFile() viewModel.showFileExporter = true - }) { + }, label: { Label("To File", systemImage: "arrow.down.doc") - } + }) Button(action: viewModel.prepareLogFile) { Label("Share", systemImage: "square.and.arrow.up") } diff --git a/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift b/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift index 333e296..9a038ce 100644 --- a/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift +++ b/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift @@ -18,14 +18,16 @@ public struct ProfileActionToolbar: View { } public var body: some View { - #if os(iOS) || os(tvOS) + #if os(iOS) iosBody + #elseif os(tvOS) + tvOSBody #elseif os(macOS) macOSBody #endif } - #if os(iOS) || os(tvOS) + #if os(iOS) private var iosBody: some View { Section("Action") { if profile.type != .remote { @@ -65,6 +67,33 @@ public struct ProfileActionToolbar: View { } #endif + #if os(tvOS) + private var tvOSBody: some View { + Section("Action") { + if profile.type == .remote { + FormButton { + viewModel.isLoading = true + Task { + await viewModel.updateProfile(profile, environments: environments) + } + } label: { + Label("Update", systemImage: "arrow.clockwise") + } + .foregroundColor(.accentColor) + .disabled(viewModel.isLoading) + } + FormButton(role: .destructive) { + Task { + await viewModel.deleteProfile(profile, environments: environments, dismiss: dismiss) + } + } label: { + Label("Delete", systemImage: "trash.fill") + } + .foregroundColor(.red) + } + } + #endif + #if os(macOS) private var macOSBody: some View { VStack(spacing: 0) { diff --git a/ApplicationLibrary/Views/Profile/ProfileView.swift b/ApplicationLibrary/Views/Profile/ProfileView.swift index 59d9623..2f18373 100644 --- a/ApplicationLibrary/Views/Profile/ProfileView.swift +++ b/ApplicationLibrary/Views/Profile/ProfileView.swift @@ -128,17 +128,17 @@ public struct ProfileView: View { if viewModel.editMode == .inactive { Button(action: { viewModel.editMode = .active - }) { + }, label: { Image(systemName: "square.and.pencil") - } + }) .tint(.accentColor) .disabled(viewModel.profileList.isEmpty) } else { Button(action: { viewModel.editMode = .inactive - }) { + }, label: { Image(systemName: "checkmark.square.fill") - } + }) .tint(.accentColor) } } diff --git a/ApplicationLibrary/Views/Profile/QRCodeSheet.swift b/ApplicationLibrary/Views/Profile/QRCodeSheet.swift index 730ad55..7a7bcc5 100644 --- a/ApplicationLibrary/Views/Profile/QRCodeSheet.swift +++ b/ApplicationLibrary/Views/Profile/QRCodeSheet.swift @@ -57,20 +57,12 @@ public struct QRCodeSheet: View { if #available(iOS 16.0, tvOS 17.0, *) { NavigationStackCompat { QRCodeContentView(profileName: profileName, remoteURL: remoteURL) - .navigationTitle("Share QR Code") - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif } .presentationDetents([.medium]) .presentationDragIndicator(.visible) } else { NavigationStackCompat { QRCodeContentView(profileName: profileName, remoteURL: remoteURL) - .navigationTitle("Share QR Code") - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif } } #endif diff --git a/ApplicationLibrary/Views/Setting/CoreView.swift b/ApplicationLibrary/Views/Setting/CoreView.swift index 6ac65c2..ee5a6ae 100644 --- a/ApplicationLibrary/Views/Setting/CoreView.swift +++ b/ApplicationLibrary/Views/Setting/CoreView.swift @@ -1,4 +1,3 @@ - import Libbox import Library import SwiftUI diff --git a/ApplicationLibrary/Views/Setting/MacAppView.swift b/ApplicationLibrary/Views/Setting/MacAppView.swift index 3c3285b..8bcc899 100644 --- a/ApplicationLibrary/Views/Setting/MacAppView.swift +++ b/ApplicationLibrary/Views/Setting/MacAppView.swift @@ -1,4 +1,3 @@ - import Library import SwiftUI diff --git a/ApplicationLibrary/Views/Setting/SettingView.swift b/ApplicationLibrary/Views/Setting/SettingView.swift index f5726eb..bfc2088 100644 --- a/ApplicationLibrary/Views/Setting/SettingView.swift +++ b/ApplicationLibrary/Views/Setting/SettingView.swift @@ -1,4 +1,3 @@ - import Library import SwiftUI diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 592d03f..3e62eaa 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -197,7 +197,14 @@ } }, "Cancel" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" + } + } + } }, "Chain" : { "localizations" : { @@ -330,6 +337,16 @@ } } }, + "Connections (%lld)" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接 (%lld)" + } + } + } + }, "Copy" : { "localizations" : { "zh-Hans" : { @@ -512,7 +529,14 @@ } }, "Do you want to save the changes you made?" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "您要保存所做的更改吗?" + } + } + } }, "Documentation" : { "localizations" : { @@ -535,7 +559,14 @@ } }, "Don't Save" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不保存" + } + } + } }, "Done" : { "localizations" : { @@ -752,6 +783,16 @@ } } }, + "Groups (%lld)" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "组 (%lld)" + } + } + } + }, "Handled unknown URL %@" : { "localizations" : { "zh-Hans" : { @@ -1028,7 +1069,14 @@ "shouldTranslate" : false }, "Manage profiles" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理配置" + } + } + } }, "Match Rule" : { "shouldTranslate" : false @@ -1500,9 +1548,6 @@ } } } - }, - "Share QR Code" : { - }, "Share URL as QR Code" : { "localizations" : { @@ -1761,7 +1806,14 @@ } }, "Unsaved Changes" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未保存的更改" + } + } + } }, "Update" : { "localizations" : { @@ -1774,7 +1826,14 @@ } }, "Update Failed" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新失败" + } + } + } }, "Uplink" : { "localizations" : { diff --git a/MacLibrary/ApplicationDelegate.swift b/MacLibrary/ApplicationDelegate.swift index 0bb5e77..33c55f7 100644 --- a/MacLibrary/ApplicationDelegate.swift +++ b/MacLibrary/ApplicationDelegate.swift @@ -63,8 +63,6 @@ open class ApplicationDelegate: NSObject, NSApplicationDelegate, UNUserNotificat switch response.actionIdentifier { case "COPY_URL": NSPasteboard.general.setString(url, forType: .URL) - case "OPEN_URL": - fallthrough default: NSWorkspace.shared.open(URL(string: url)!) } diff --git a/MacLibrary/MainView.swift b/MacLibrary/MainView.swift index 79e6de7..5bc7ffd 100644 --- a/MacLibrary/MainView.swift +++ b/MacLibrary/MainView.swift @@ -46,7 +46,7 @@ public struct MainView: View { Label("Dashboard Items", systemImage: "square.grid.2x2") } } label: { - Label("Others", systemImage: "ellipsis.circle") + Label("Others", systemImage: "line.3.horizontal.circle") } } } @@ -65,9 +65,11 @@ public struct MainView: View { .environment(\.profileEditor, profileEditor) .handlesExternalEvents(preferring: [], allowing: ["*"]) .onOpenURL(perform: viewModel.openURL) - .sheet(isPresented: $showCardManagement) { - CardManagementSheet(configurationVersion: $cardConfigurationVersion) + .sheet(isPresented: $showCardManagement, onDismiss: { + cardConfigurationVersion += 1 + }, content: { + CardManagementSheet() .frame(minWidth: 400, minHeight: 400) - } + }) } } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0df4b1c --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +all: ios macos macos_standalone tvos + +ios: + xcodebuild build -scheme SFI -configuration Debug -destination 'generic/platform=iOS' | xcbeautify | grep -A 3 -e "Build Succeeded" -e "BUILD FAILED" -e "❌" + +macos: + xcodebuild build -scheme SFM -configuration Debug -destination 'generic/platform=macOS' | xcbeautify | grep -A 3 -e "Build Succeeded" -e "BUILD FAILED" -e "❌" + +macos_standalone: + xcodebuild build -scheme SFM.System -configuration Debug -destination 'generic/platform=macOS' | xcbeautify | grep -A 3 -e "Build Succeeded" -e "BUILD FAILED" -e "❌" + +tvos: + xcodebuild build -scheme SFT -configuration Debug -destination 'generic/platform=tvOS' | xcbeautify | grep -A 3 -e "Build Succeeded" -e "BUILD FAILED" -e "❌" + +fmt: + swiftformat . + +fmt_install: + brew install swiftformat + +lint: + swiftlint + +lint_install: + brew install swiftlint diff --git a/SFI/ApplicationDelegate.swift b/SFI/ApplicationDelegate.swift index 7eda312..a9db138 100644 --- a/SFI/ApplicationDelegate.swift +++ b/SFI/ApplicationDelegate.swift @@ -44,8 +44,6 @@ class ApplicationDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCe switch response.actionIdentifier { case "COPY_URL": UIPasteboard.general.string = url - case "OPEN_URL": - fallthrough default: await UIApplication.shared.open(URL(string: url)!) } diff --git a/SFI/MainView.swift b/SFI/MainView.swift index f8b20b4..45036f4 100644 --- a/SFI/MainView.swift +++ b/SFI/MainView.swift @@ -51,8 +51,8 @@ struct MainView: View { HStack(spacing: 12) { if let profile = environments.extensionProfile { StatusText(profile: profile) + .frame(maxWidth: .infinity, alignment: .leading) } - Spacer() NavigationButtonsView( showGroupsButton: buttonState.showGroupsButton, showConnectionsButton: buttonState.showConnectionsButton, @@ -187,6 +187,8 @@ struct MainView: View { Text(statusText) .font(.subheadline) .foregroundStyle(.secondary) + .lineLimit(1) + .fixedSize() } private var statusText: String {