Add swiftlint & Minor fixes

This commit is contained in:
世界
2025-11-27 16:10:55 +08:00
parent 8855e0eef9
commit 0bfb6d2516
29 changed files with 382 additions and 196 deletions
+21
View File
@@ -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
@@ -27,20 +27,6 @@ public struct NavigationButtonsView: View {
public var body: some View { public var body: some View {
HStack(spacing: 12) { 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 { if showConnectionsButton {
Divider() Divider()
Text(verbatim: "\(connectionsCount)") Text(verbatim: "\(connectionsCount)")
@@ -55,6 +41,20 @@ public struct NavigationButtonsView: View {
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.foregroundStyle(.primary) .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)
}
} }
} }
} }
@@ -24,13 +24,12 @@ public struct Connection: Codable {
public let outboundType: String public let outboundType: String
public let chain: [String] public let chain: [String]
var hashValue: Int { func hash(into hasher: inout Hasher) {
var value = id.hashValue hasher.combine(id)
(value, _) = value.addingReportingOverflow(upload.hashValue) hasher.combine(upload)
(value, _) = value.addingReportingOverflow(download.hashValue) hasher.combine(download)
(value, _) = value.addingReportingOverflow(uploadTotal.hashValue) hasher.combine(uploadTotal)
(value, _) = value.addingReportingOverflow(downloadTotal.hashValue) hasher.combine(downloadTotal)
return value
} }
func performSearch(_ content: String) -> Bool { func performSearch(_ content: String) -> Bool {
@@ -17,7 +17,7 @@ public struct ConnectionListView: View {
} else { } else {
ScrollView { ScrollView {
LazyVGrid(columns: [GridItem(.flexible())], alignment: .leading) { LazyVGrid(columns: [GridItem(.flexible())], alignment: .leading) {
ForEach(viewModel.filteredConnections(), id: \.hashValue) { it in ForEach(viewModel.filteredConnections(), id: \.id) { it in
ConnectionView(it) ConnectionView(it)
} }
} }
@@ -56,9 +56,19 @@ public struct ConnectionListView: View {
#endif #endif
.alertBinding($viewModel.alert) .alertBinding($viewModel.alert)
.onAppear { .onAppear {
viewModel.setCommandClient(environments.commandClient)
viewModel.connect() 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) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
#if os(iOS) #if os(iOS)
.background(Color(uiColor: .systemGroupedBackground)) .background(Color(uiColor: .systemGroupedBackground))
@@ -1,4 +1,3 @@
import Combine
import Libbox import Libbox
import Library import Library
import SwiftUI import SwiftUI
@@ -11,8 +10,6 @@ public class ConnectionListViewModel: ObservableObject {
@Published public var alert: Alert? @Published public var alert: Alert?
@Published public var connectionStateFilter: ConnectionStateFilter { @Published public var connectionStateFilter: ConnectionStateFilter {
didSet { didSet {
commandClient?.connectionStateFilter = connectionStateFilter
commandClient?.filterConnectionsNow()
saveStateFilterTask?.cancel() saveStateFilterTask?.cancel()
saveStateFilterTask = Task { saveStateFilterTask = Task {
await SharedPreferences.connectionStateFilter.set(connectionStateFilter.rawValue) await SharedPreferences.connectionStateFilter.set(connectionStateFilter.rawValue)
@@ -22,8 +19,6 @@ public class ConnectionListViewModel: ObservableObject {
@Published public var connectionSort: ConnectionSort { @Published public var connectionSort: ConnectionSort {
didSet { didSet {
commandClient?.connectionSort = connectionSort
commandClient?.filterConnectionsNow()
saveSortTask?.cancel() saveSortTask?.cancel()
saveSortTask = Task { saveSortTask = Task {
await SharedPreferences.connectionSort.set(connectionSort.rawValue) await SharedPreferences.connectionSort.set(connectionSort.rawValue)
@@ -31,8 +26,6 @@ public class ConnectionListViewModel: ObservableObject {
} }
} }
private var commandClient: CommandClient?
private var cancellables = Set<AnyCancellable>()
private var connectTask: Task<Void, Never>? private var connectTask: Task<Void, Never>?
private var saveStateFilterTask: Task<Void, Never>? private var saveStateFilterTask: Task<Void, Never>?
private var saveSortTask: Task<Void, Never>? private var saveSortTask: Task<Void, Never>?
@@ -42,16 +35,6 @@ public class ConnectionListViewModel: ObservableObject {
connectionSort = .byDate 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() { public func connect() {
if ApplicationLibrary.inPreview { if ApplicationLibrary.inPreview {
isLoading = false 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) connections = convertConnections(goConnections)
isLoading = false isLoading = false
} }
@@ -111,22 +111,25 @@ public struct ActiveDashboardView: View {
#if os(iOS) || os(tvOS) #if os(iOS) || os(tvOS)
private func updateButtonVisibility() { 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 @ToolbarContentBuilder
private var toolbar: some ToolbarContent { private var toolbar: some ToolbarContent {
ToolbarItem(placement: .topBarLeading) { #if os(tvOS)
#if os(iOS) ToolbarItem(placement: .topBarLeading) {
if #available(iOS 26.0, *), !Variant.debugNoIOS26 {
EmptyView()
} else {
navigationButtons
}
#else
navigationButtons navigationButtons
#endif }
} #endif
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
if #available(iOS 16.0, tvOS 17.0, *) { if #available(iOS 16.0, tvOS 17.0, *) {
cardManagementButton cardManagementButton
@@ -137,10 +140,7 @@ public struct ActiveDashboardView: View {
if #available(iOS 26.0, *), !Variant.debugNoIOS26 { if #available(iOS 26.0, *), !Variant.debugNoIOS26 {
EmptyView() EmptyView()
} else { } else {
HStack(spacing: 12) { StartStopButton()
Divider()
StartStopButton()
}
} }
#else #else
HStack(spacing: 12) { HStack(spacing: 12) {
@@ -151,16 +151,18 @@ public struct ActiveDashboardView: View {
} }
} }
private var navigationButtons: some View { #if os(tvOS)
NavigationButtonsView( private var navigationButtons: some View {
showGroupsButton: buttonState.showGroupsButton, NavigationButtonsView(
showConnectionsButton: buttonState.showConnectionsButton, showGroupsButton: buttonState.showGroupsButton,
groupsCount: buttonState.groupsCount, showConnectionsButton: buttonState.showConnectionsButton,
connectionsCount: buttonState.connectionsCount, groupsCount: buttonState.groupsCount,
onGroupsTap: { showGroups = true }, connectionsCount: buttonState.connectionsCount,
onConnectionsTap: { showConnections = true } onGroupsTap: { showGroups = true },
) onConnectionsTap: { showConnections = true }
} )
}
#endif
#endif #endif
#if os(iOS) || os(tvOS) #if os(iOS) || os(tvOS)
@@ -176,19 +178,42 @@ public struct ActiveDashboardView: View {
@ViewBuilder @ViewBuilder
private var cardManagementButton: some View { private var cardManagementButton: some View {
Menu { 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 { Button {
showCardManagement = true showCardManagement = true
} label: { } label: {
Label("Dashboard Items", systemImage: "square.grid.2x2") Label("Dashboard Items", systemImage: "square.grid.2x2")
} }
} label: { } label: {
Label("Others", systemImage: "ellipsis.circle") Label("Others", systemImage: "line.3.horizontal.circle")
} }
.sheet(isPresented: $showCardManagement) { .sheet(isPresented: $showCardManagement, onDismiss: {
CardManagementSheet(configurationVersion: $cardConfigurationVersion) cardConfigurationVersion += 1
}, content: {
CardManagementSheet()
.presentationDetents([.large]) .presentationDetents([.large])
.presentationDragIndicator(.visible) .presentationDragIndicator(.visible)
} })
} }
#endif #endif
} }
@@ -4,11 +4,8 @@ import SwiftUI
@MainActor public struct CardManagementSheet: View { @MainActor public struct CardManagementSheet: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@StateObject private var configuration = DashboardCardConfiguration() @StateObject private var configuration = DashboardCardConfiguration()
@Binding private var configurationVersion: Int
public init(configurationVersion: Binding<Int>) { public init() {}
_configurationVersion = configurationVersion
}
public var body: some View { public var body: some View {
#if os(macOS) #if os(macOS)
@@ -41,7 +38,6 @@ import SwiftUI
Button("Reset", role: .destructive) { Button("Reset", role: .destructive) {
Task { Task {
await configuration.resetToDefault() await configuration.resetToDefault()
configurationVersion += 1
} }
} }
} }
@@ -49,11 +45,9 @@ import SwiftUI
Button("Done") { Button("Done") {
dismiss() dismiss()
} }
.keyboardShortcut(.escape, modifiers: [])
} }
} }
.onExitCommand {
dismiss()
}
} }
#else #else
private var iOSBody: some View { private var iOSBody: some View {
@@ -74,7 +68,6 @@ import SwiftUI
Button("Reset", role: .destructive) { Button("Reset", role: .destructive) {
Task { Task {
await configuration.resetToDefault() await configuration.resetToDefault()
configurationVersion += 1
} }
} }
} }
@@ -91,13 +84,13 @@ import SwiftUI
isEnabled: configuration.isEnabled(card), isEnabled: configuration.isEnabled(card),
onToggle: { onToggle: {
configuration.toggleCard(card) configuration.toggleCard(card)
configurationVersion += 1
} }
) )
} }
.onMove { source, destination in .onMove { source, destination in
configuration.moveCard(from: source, to: destination) Task {
configurationVersion += 1 await configuration.moveCard(from: source, to: destination)
}
} }
} }
.applyContentMargins() .applyContentMargins()
@@ -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) cardOrder.move(fromOffsets: source, toOffset: destination)
await saveCardOrder()
// Save asynchronously in background
Task {
await saveCardOrder()
}
} }
public func resetToDefault() async { public func resetToDefault() async {
@@ -19,12 +19,17 @@ public struct HTTPProxyCard: View {
public var body: some View { public var body: some View {
DashboardCardView(title: "", isHalfWidth: false) { DashboardCardView(title: "", isHalfWidth: false) {
Toggle("System HTTP Proxy", isOn: $systemProxyEnabled) HStack {
.onChangeCompat(of: systemProxyEnabled) { newValue in Text("System HTTP Proxy")
Task { Spacer()
await onToggle(newValue) Toggle("", isOn: $systemProxyEnabled)
.labelsHidden()
.onChangeCompat(of: systemProxyEnabled) { newValue in
Task {
await onToggle(newValue)
}
} }
} }
} }
} }
} }
@@ -34,10 +34,10 @@ public struct ProfileCard: View {
.disabled(viewModel.isUpdating) .disabled(viewModel.isUpdating)
.sheet(isPresented: $viewModel.showNewProfile, onDismiss: { .sheet(isPresented: $viewModel.showNewProfile, onDismiss: {
environments.profileUpdate.send() environments.profileUpdate.send()
}) { }, content: {
NewProfileNavigationView() NewProfileNavigationView()
.environmentObject(environments) .environmentObject(environments)
} })
.sheet(isPresented: $viewModel.showManageProfiles) { .sheet(isPresented: $viewModel.showManageProfiles) {
manageProfilesSheet manageProfilesSheet
} }
@@ -218,11 +218,12 @@ public struct ProfileCard: View {
NavigationSheet( NavigationSheet(
title: String(localized: "Manage profiles"), title: String(localized: "Manage profiles"),
showDoneButton: true, showDoneButton: true,
onDismiss: { viewModel.showManageProfiles = false } onDismiss: { viewModel.showManageProfiles = false },
) { content: {
ManageProfilesView() ManageProfilesView()
.environmentObject(environments) .environmentObject(environments)
} }
)
} }
@ViewBuilder @ViewBuilder
@@ -390,6 +391,7 @@ extension ProfileCard {
@ObservedObject private var viewModel: ProfileViewModel @ObservedObject private var viewModel: ProfileViewModel
@State private var profile: ProfilePreview @State private var profile: ProfilePreview
@State private var shareLinkPresented = false @State private var shareLinkPresented = false
@State private var isUpdating = false
init(_ viewModel: ProfileViewModel, _ profile: ProfilePreview) { init(_ viewModel: ProfileViewModel, _ profile: ProfilePreview) {
self.viewModel = viewModel self.viewModel = viewModel
@@ -416,25 +418,26 @@ extension ProfileCard {
HStack(spacing: 8) { HStack(spacing: 8) {
if profile.type == .remote { if profile.type == .remote {
Button { Button {
viewModel.isUpdating = true isUpdating = true
Task { Task {
await viewModel.updateProfile(profile.origin) await viewModel.updateProfile(profile.origin)
profile = ProfilePreview(profile.origin) profile = ProfilePreview(profile.origin)
isUpdating = false
} }
} label: { } label: {
Image(systemName: "arrow.clockwise") Image(systemName: "arrow.clockwise")
.font(.system(size: 16)) .font(.system(size: 16))
.rotationEffect(.degrees(viewModel.isUpdating ? 360 : 0)) .rotationEffect(.degrees(isUpdating ? 360 : 0))
.animation( .animation(
viewModel.isUpdating isUpdating
? .linear(duration: 1).repeatForever(autoreverses: false) ? .linear(duration: 1).repeatForever(autoreverses: false)
: .default, : .default,
value: viewModel.isUpdating value: isUpdating
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.actionButtonStyle() .actionButtonStyle()
.disabled(viewModel.isUpdating) .disabled(isUpdating)
Button { Button {
shareLinkPresented = true shareLinkPresented = true
@@ -83,10 +83,8 @@ struct ProfileSelectorButton: View {
private func updateButtonContent(_ button: MenuAttachmentButton) { private func updateButtonContent(_ button: MenuAttachmentButton) {
// Remove existing subviews // Remove existing subviews
for subview in button.subviews { for subview in button.subviews where subview is UIStackView {
if subview is UIStackView { subview.removeFromSuperview()
subview.removeFromSuperview()
}
} }
// Create content stack // Create content stack
@@ -42,35 +42,72 @@ public struct StartStopButton: View {
await switchProfile(!profile.status.isConnected) await switchProfile(!profile.status.isConnected)
} }
} label: { } label: {
HStack(spacing: 8) { #if os(iOS)
if profile.status.isConnectedStrict, let duration = runtimeDuration { HStack(spacing: 8) {
Text(duration) if showRuntimeDuration, profile.status.isConnectedStrict, let duration = runtimeDuration {
.font(.caption) Text(duration)
.foregroundStyle(.secondary) .font(.caption)
.monospacedDigit() .foregroundStyle(.secondary)
.transition(.asymmetric( .monospacedDigit()
insertion: .move(edge: .trailing).combined(with: .opacity), .fixedSize()
removal: .move(edge: .trailing).combined(with: .opacity) .transition(.asymmetric(
)) insertion: .move(edge: .trailing).combined(with: .opacity),
} removal: .move(edge: .trailing).combined(with: .opacity)
))
}
if !profile.status.isConnected { if !profile.status.isConnected {
Label("Start", systemImage: "play.fill") Label("Start", systemImage: "play.fill")
} else { } else {
Label("Stop", systemImage: "stop.fill") 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) .labelStyle(.iconOnly)
.tint(.primary) #if os(iOS)
.disabled(!profile.status.isEnabled) .modifier(PrimaryTintModifier())
.alertBinding($alert) #else
.onReceive(timer) { _ in .tint(.primary)
currentTime = Date() #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? { private var runtimeDuration: String? {
guard let connectedDate = profile.connectedDate else { return nil } guard let connectedDate = profile.connectedDate else { return nil }
let interval = currentTime.timeIntervalSince(connectedDate) 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
@@ -85,10 +85,10 @@ public struct DashboardView: View {
private func importRemoteProfileSheet(for request: NewProfileView.ImportRequest) -> some View { private func importRemoteProfileSheet(for request: NewProfileView.ImportRequest) -> some View {
NavigationSheet(title: "Import Profile", onDismiss: { NavigationSheet(title: "Import Profile", onDismiss: {
environments.profileUpdate.send() environments.profileUpdate.send()
}) { }, content: {
NewProfileView(request) NewProfileView(request)
.environmentObject(environments) .environmentObject(environments)
} })
} }
@ViewBuilder @ViewBuilder
@@ -111,11 +111,11 @@ public struct DashboardView: View {
@ViewBuilder @ViewBuilder
private var mainContent: some View { private var mainContent: some View {
if ApplicationLibrary.inPreview { if ApplicationLibrary.inPreview {
ActiveDashboardView(externalCardConfigurationVersion: cardConfigurationVersion) activeDashboardView
} else if environments.extensionProfileLoading { } else if environments.extensionProfileLoading {
ProgressView() ProgressView()
} else if let profile = environments.extensionProfile { } else if let profile = environments.extensionProfile {
ActiveDashboardView(externalCardConfigurationVersion: cardConfigurationVersion) activeDashboardView
.environmentObject(profile) .environmentObject(profile)
.alertBinding($coordinator.alert) .alertBinding($coordinator.alert)
.onChangeCompat(of: profile.status) { status in .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
}
} }
@@ -23,8 +23,10 @@ public struct GroupListView: View {
} }
} }
.onAppear { .onAppear {
viewModel.setCommandClient(environments.commandClient)
viewModel.connect() viewModel.connect()
} }
.onReceive(environments.commandClient.$groups) { groups in
viewModel.setGroups(groups)
}
} }
} }
@@ -1,4 +1,3 @@
import Combine
import Libbox import Libbox
import Library import Library
import SwiftUI import SwiftUI
@@ -8,21 +7,8 @@ public class GroupListViewModel: ObservableObject {
@Published public var isLoading = true @Published public var isLoading = true
@Published public var groups: [OutboundGroup] = [] @Published public var groups: [OutboundGroup] = []
private var commandClient: CommandClient?
private var cancellables = Set<AnyCancellable>()
public init() {} 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() { public func connect() {
if ApplicationLibrary.inPreview { if ApplicationLibrary.inPreview {
groups = [ 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]() var groups = [OutboundGroup]()
for goGroup in goGroups { for goGroup in goGroups {
var items = [OutboundGroupItem]() var items = [OutboundGroupItem]()
@@ -2,7 +2,7 @@ import Foundation
import Libbox import Libbox
import SwiftUI import SwiftUI
public struct OutboundGroup: Codable { public struct OutboundGroup: Codable, Hashable {
let tag: String let tag: String
let type: String let type: String
var selected: String var selected: String
@@ -10,13 +10,16 @@ public struct OutboundGroup: Codable {
var isExpand: Bool var isExpand: Bool
let items: [OutboundGroupItem] let items: [OutboundGroupItem]
var hashValue: Int { public func hash(into hasher: inout Hasher) {
var value = tag.hashValue hasher.combine(tag)
(value, _) = value.addingReportingOverflow(selected.hashValue) hasher.combine(selected)
for item in items { 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
} }
} }
+3 -3
View File
@@ -85,7 +85,7 @@ private struct LogViewContent: View {
.focusEffectDisabled() .focusEffectDisabled()
.focusSection() .focusSection()
#else #else
let previewLogs = logList.enumerated().map { _, message in let previewLogs = logList.map { message in
LogEntry(level: 4, message: message) LogEntry(level: 4, message: message)
} }
return LogTextView( return LogTextView(
@@ -205,9 +205,9 @@ private struct LogViewContent: View {
Button(action: { Button(action: {
viewModel.prepareLogFile() viewModel.prepareLogFile()
viewModel.showFileExporter = true viewModel.showFileExporter = true
}) { }, label: {
Label("To File", systemImage: "arrow.down.doc") Label("To File", systemImage: "arrow.down.doc")
} })
Button(action: viewModel.prepareLogFile) { Button(action: viewModel.prepareLogFile) {
Label("Share", systemImage: "square.and.arrow.up") Label("Share", systemImage: "square.and.arrow.up")
} }
@@ -18,14 +18,16 @@ public struct ProfileActionToolbar: View {
} }
public var body: some View { public var body: some View {
#if os(iOS) || os(tvOS) #if os(iOS)
iosBody iosBody
#elseif os(tvOS)
tvOSBody
#elseif os(macOS) #elseif os(macOS)
macOSBody macOSBody
#endif #endif
} }
#if os(iOS) || os(tvOS) #if os(iOS)
private var iosBody: some View { private var iosBody: some View {
Section("Action") { Section("Action") {
if profile.type != .remote { if profile.type != .remote {
@@ -65,6 +67,33 @@ public struct ProfileActionToolbar: View {
} }
#endif #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) #if os(macOS)
private var macOSBody: some View { private var macOSBody: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -128,17 +128,17 @@ public struct ProfileView: View {
if viewModel.editMode == .inactive { if viewModel.editMode == .inactive {
Button(action: { Button(action: {
viewModel.editMode = .active viewModel.editMode = .active
}) { }, label: {
Image(systemName: "square.and.pencil") Image(systemName: "square.and.pencil")
} })
.tint(.accentColor) .tint(.accentColor)
.disabled(viewModel.profileList.isEmpty) .disabled(viewModel.profileList.isEmpty)
} else { } else {
Button(action: { Button(action: {
viewModel.editMode = .inactive viewModel.editMode = .inactive
}) { }, label: {
Image(systemName: "checkmark.square.fill") Image(systemName: "checkmark.square.fill")
} })
.tint(.accentColor) .tint(.accentColor)
} }
} }
@@ -57,20 +57,12 @@ public struct QRCodeSheet: View {
if #available(iOS 16.0, tvOS 17.0, *) { if #available(iOS 16.0, tvOS 17.0, *) {
NavigationStackCompat { NavigationStackCompat {
QRCodeContentView(profileName: profileName, remoteURL: remoteURL) QRCodeContentView(profileName: profileName, remoteURL: remoteURL)
.navigationTitle("Share QR Code")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
} }
.presentationDetents([.medium]) .presentationDetents([.medium])
.presentationDragIndicator(.visible) .presentationDragIndicator(.visible)
} else { } else {
NavigationStackCompat { NavigationStackCompat {
QRCodeContentView(profileName: profileName, remoteURL: remoteURL) QRCodeContentView(profileName: profileName, remoteURL: remoteURL)
.navigationTitle("Share QR Code")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
} }
} }
#endif #endif
@@ -1,4 +1,3 @@
import Libbox import Libbox
import Library import Library
import SwiftUI import SwiftUI
@@ -1,4 +1,3 @@
import Library import Library
import SwiftUI import SwiftUI
@@ -1,4 +1,3 @@
import Library import Library
import SwiftUI import SwiftUI
+68 -9
View File
@@ -197,7 +197,14 @@
} }
}, },
"Cancel" : { "Cancel" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "取消"
}
}
}
}, },
"Chain" : { "Chain" : {
"localizations" : { "localizations" : {
@@ -330,6 +337,16 @@
} }
} }
}, },
"Connections (%lld)" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "连接 (%lld)"
}
}
}
},
"Copy" : { "Copy" : {
"localizations" : { "localizations" : {
"zh-Hans" : { "zh-Hans" : {
@@ -512,7 +529,14 @@
} }
}, },
"Do you want to save the changes you made?" : { "Do you want to save the changes you made?" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "您要保存所做的更改吗?"
}
}
}
}, },
"Documentation" : { "Documentation" : {
"localizations" : { "localizations" : {
@@ -535,7 +559,14 @@
} }
}, },
"Don't Save" : { "Don't Save" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "不保存"
}
}
}
}, },
"Done" : { "Done" : {
"localizations" : { "localizations" : {
@@ -752,6 +783,16 @@
} }
} }
}, },
"Groups (%lld)" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "组 (%lld)"
}
}
}
},
"Handled unknown URL %@" : { "Handled unknown URL %@" : {
"localizations" : { "localizations" : {
"zh-Hans" : { "zh-Hans" : {
@@ -1028,7 +1069,14 @@
"shouldTranslate" : false "shouldTranslate" : false
}, },
"Manage profiles" : { "Manage profiles" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "管理配置"
}
}
}
}, },
"Match Rule" : { "Match Rule" : {
"shouldTranslate" : false "shouldTranslate" : false
@@ -1500,9 +1548,6 @@
} }
} }
} }
},
"Share QR Code" : {
}, },
"Share URL as QR Code" : { "Share URL as QR Code" : {
"localizations" : { "localizations" : {
@@ -1761,7 +1806,14 @@
} }
}, },
"Unsaved Changes" : { "Unsaved Changes" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "未保存的更改"
}
}
}
}, },
"Update" : { "Update" : {
"localizations" : { "localizations" : {
@@ -1774,7 +1826,14 @@
} }
}, },
"Update Failed" : { "Update Failed" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "更新失败"
}
}
}
}, },
"Uplink" : { "Uplink" : {
"localizations" : { "localizations" : {
-2
View File
@@ -63,8 +63,6 @@ open class ApplicationDelegate: NSObject, NSApplicationDelegate, UNUserNotificat
switch response.actionIdentifier { switch response.actionIdentifier {
case "COPY_URL": case "COPY_URL":
NSPasteboard.general.setString(url, forType: .URL) NSPasteboard.general.setString(url, forType: .URL)
case "OPEN_URL":
fallthrough
default: default:
NSWorkspace.shared.open(URL(string: url)!) NSWorkspace.shared.open(URL(string: url)!)
} }
+6 -4
View File
@@ -46,7 +46,7 @@ public struct MainView: View {
Label("Dashboard Items", systemImage: "square.grid.2x2") Label("Dashboard Items", systemImage: "square.grid.2x2")
} }
} label: { } label: {
Label("Others", systemImage: "ellipsis.circle") Label("Others", systemImage: "line.3.horizontal.circle")
} }
} }
} }
@@ -65,9 +65,11 @@ public struct MainView: View {
.environment(\.profileEditor, profileEditor) .environment(\.profileEditor, profileEditor)
.handlesExternalEvents(preferring: [], allowing: ["*"]) .handlesExternalEvents(preferring: [], allowing: ["*"])
.onOpenURL(perform: viewModel.openURL) .onOpenURL(perform: viewModel.openURL)
.sheet(isPresented: $showCardManagement) { .sheet(isPresented: $showCardManagement, onDismiss: {
CardManagementSheet(configurationVersion: $cardConfigurationVersion) cardConfigurationVersion += 1
}, content: {
CardManagementSheet()
.frame(minWidth: 400, minHeight: 400) .frame(minWidth: 400, minHeight: 400)
} })
} }
} }
+25
View File
@@ -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
-2
View File
@@ -44,8 +44,6 @@ class ApplicationDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCe
switch response.actionIdentifier { switch response.actionIdentifier {
case "COPY_URL": case "COPY_URL":
UIPasteboard.general.string = url UIPasteboard.general.string = url
case "OPEN_URL":
fallthrough
default: default:
await UIApplication.shared.open(URL(string: url)!) await UIApplication.shared.open(URL(string: url)!)
} }
+3 -1
View File
@@ -51,8 +51,8 @@ struct MainView: View {
HStack(spacing: 12) { HStack(spacing: 12) {
if let profile = environments.extensionProfile { if let profile = environments.extensionProfile {
StatusText(profile: profile) StatusText(profile: profile)
.frame(maxWidth: .infinity, alignment: .leading)
} }
Spacer()
NavigationButtonsView( NavigationButtonsView(
showGroupsButton: buttonState.showGroupsButton, showGroupsButton: buttonState.showGroupsButton,
showConnectionsButton: buttonState.showConnectionsButton, showConnectionsButton: buttonState.showConnectionsButton,
@@ -187,6 +187,8 @@ struct MainView: View {
Text(statusText) Text(statusText)
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.lineLimit(1)
.fixedSize()
} }
private var statusText: String { private var statusText: String {