refactored code structure
This commit is contained in:
7
NOTES.md
Normal file
7
NOTES.md
Normal file
@@ -0,0 +1,7 @@
|
||||
- add tooltip for:
|
||||
Ping (est time consideration)
|
||||
dynamic Data
|
||||
static Data
|
||||
|
||||
move source to /src. like iKeyMon before
|
||||
add a merker for "reboot required"
|
||||
@@ -98,6 +98,43 @@ struct ServerInfo: Codable, Hashable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
struct OperatingSystem: Codable, Hashable, Equatable {
|
||||
struct UpdateStatus: Codable, Hashable, Equatable {
|
||||
let updateCount: Int
|
||||
let securityUpdateCount: Int
|
||||
let rebootRequired: Bool
|
||||
|
||||
init(updateCount: Int, securityUpdateCount: Int, rebootRequired: Bool) {
|
||||
self.updateCount = updateCount
|
||||
self.securityUpdateCount = securityUpdateCount
|
||||
self.rebootRequired = rebootRequired
|
||||
}
|
||||
}
|
||||
|
||||
let label: String
|
||||
let distribution: String
|
||||
let version: String
|
||||
let architecture: String
|
||||
let endOfLife: Bool
|
||||
let updates: UpdateStatus?
|
||||
|
||||
init(
|
||||
label: String,
|
||||
distribution: String,
|
||||
version: String,
|
||||
architecture: String,
|
||||
endOfLife: Bool,
|
||||
updates: UpdateStatus?
|
||||
) {
|
||||
self.label = label
|
||||
self.distribution = distribution
|
||||
self.version = version
|
||||
self.architecture = architecture
|
||||
self.endOfLife = endOfLife
|
||||
self.updates = updates
|
||||
}
|
||||
}
|
||||
|
||||
var hostname: String
|
||||
var ipAddresses: [String]
|
||||
var cpuCores: Int
|
||||
@@ -108,6 +145,7 @@ struct ServerInfo: Codable, Hashable, Equatable {
|
||||
var phpVersion: String
|
||||
var mysqlVersion: String?
|
||||
var mariadbVersion: String?
|
||||
var operatingSystem: OperatingSystem?
|
||||
var ports: [ServicePort]?
|
||||
var load: Load
|
||||
var memory: Memory
|
||||
@@ -128,6 +166,15 @@ struct ServerInfo: Codable, Hashable, Equatable {
|
||||
}
|
||||
return ServerInfo.displayFormatter.string(from: date)
|
||||
}
|
||||
|
||||
var operatingSystemSummary: String? {
|
||||
guard let operatingSystem else { return nil }
|
||||
let components = [
|
||||
operatingSystem.label,
|
||||
operatingSystem.architecture
|
||||
].filter { !$0.isEmpty }
|
||||
return components.isEmpty ? nil : components.joined(separator: " • ")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers & Sample Data
|
||||
@@ -157,6 +204,18 @@ extension ServerInfo {
|
||||
phpVersion: "8.2.12",
|
||||
mysqlVersion: "8.0.33",
|
||||
mariadbVersion: nil,
|
||||
operatingSystem: OperatingSystem(
|
||||
label: "Debian 12.12 (64-bit)",
|
||||
distribution: "Debian",
|
||||
version: "12.12",
|
||||
architecture: "x86_64",
|
||||
endOfLife: false,
|
||||
updates: OperatingSystem.UpdateStatus(
|
||||
updateCount: 12,
|
||||
securityUpdateCount: 8,
|
||||
rebootRequired: true
|
||||
)
|
||||
),
|
||||
ports: [
|
||||
ServicePort(service: "HTTP", status: "online", port: 80, proto: "tcp"),
|
||||
ServicePort(service: "HTTPS", status: "online", port: 443, proto: "tcp"),
|
||||
@@ -196,6 +196,7 @@ class APIv2_12: BaseAPIClient, ServerAPIProtocol {
|
||||
private extension APIv2_12 {
|
||||
struct ServerSummaryEnvelope: Decodable {
|
||||
let meta: Meta
|
||||
let operatingSystem: OperatingSystem?
|
||||
let utilization: Utilization
|
||||
let components: Components
|
||||
let ports: [Port]?
|
||||
@@ -279,6 +280,36 @@ private extension APIv2_12 {
|
||||
let path: String?
|
||||
let configFile: String?
|
||||
}
|
||||
|
||||
struct OperatingSystem: Decodable {
|
||||
struct Updates: Decodable {
|
||||
let updateCount: Int
|
||||
let securityUpdateCount: Int
|
||||
let rebootRequired: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case updateCount = "update_count"
|
||||
case securityUpdateCount = "security_update_count"
|
||||
case rebootRequired = "reboot_required"
|
||||
}
|
||||
}
|
||||
|
||||
let label: String
|
||||
let distribution: String
|
||||
let version: String
|
||||
let architecture: String
|
||||
let endOfLife: Bool
|
||||
let updates: Updates?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case label
|
||||
case distribution
|
||||
case version
|
||||
case architecture
|
||||
case endOfLife = "end_of_life"
|
||||
case updates
|
||||
}
|
||||
}
|
||||
|
||||
func toDomain() -> ServerInfo {
|
||||
let load = utilization.load
|
||||
@@ -297,6 +328,22 @@ private extension APIv2_12 {
|
||||
phpVersion: components.php,
|
||||
mysqlVersion: components.mysql,
|
||||
mariadbVersion: components.mariadb,
|
||||
operatingSystem: operatingSystem.map {
|
||||
ServerInfo.OperatingSystem(
|
||||
label: $0.label,
|
||||
distribution: $0.distribution,
|
||||
version: $0.version,
|
||||
architecture: $0.architecture,
|
||||
endOfLife: $0.endOfLife,
|
||||
updates: $0.updates.map {
|
||||
ServerInfo.OperatingSystem.UpdateStatus(
|
||||
updateCount: $0.updateCount,
|
||||
securityUpdateCount: $0.securityUpdateCount,
|
||||
rebootRequired: $0.rebootRequired
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
ports: ports?.map {
|
||||
ServerInfo.ServicePort(service: $0.service, status: $0.status, port: $0.port, proto: $0.proto)
|
||||
},
|
||||
242
Sources/Views/PreferencesView.swift
Normal file
242
Sources/Views/PreferencesView.swift
Normal file
@@ -0,0 +1,242 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PreferencesView: View {
|
||||
private enum Tab: CaseIterable {
|
||||
case monitor, notifications, alerts
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .monitor: return "Monitor"
|
||||
case .notifications: return "Notifications"
|
||||
case .alerts: return "Alerts"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .monitor: return "waveform.path.ecg"
|
||||
case .notifications: return "bell.badge"
|
||||
case .alerts: return "exclamationmark.triangle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AppStorage("pingInterval") private var storedPingInterval: Int = 10
|
||||
@AppStorage("refreshInterval") private var storedRefreshInterval: Int = 60
|
||||
@AppStorage("showIntervalIndicator") private var showIntervalIndicator: Bool = true
|
||||
|
||||
@State private var pingIntervalSlider: Double = 10
|
||||
@State private var refreshIntervalSlider: Double = 60
|
||||
@State private var selection: Tab = .monitor
|
||||
|
||||
private let minimumInterval: Double = 10
|
||||
private let maximumPingInterval: Double = 600
|
||||
private let maximumRefreshInterval: Double = 600
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
sidebar
|
||||
Divider()
|
||||
ScrollView {
|
||||
detailContent(for: selection)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
}
|
||||
.frame(minWidth: 360, maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.frame(minWidth: 560, minHeight: 360)
|
||||
.onAppear {
|
||||
pingIntervalSlider = Double(storedPingInterval)
|
||||
refreshIntervalSlider = Double(storedRefreshInterval)
|
||||
}
|
||||
.onChange(of: pingIntervalSlider) { _, newValue in
|
||||
storedPingInterval = Int(newValue)
|
||||
}
|
||||
.onChange(of: refreshIntervalSlider) { _, newValue in
|
||||
storedRefreshInterval = Int(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
private var sidebar: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Preferences")
|
||||
.font(.headline)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
ForEach(Tab.allCases, id: \.self) { tab in
|
||||
Button {
|
||||
selection = tab
|
||||
} label: {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: tab.icon)
|
||||
.frame(width: 20)
|
||||
Text(tab.title)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(selection == tab ? Color.accentColor.opacity(0.25) : Color.clear)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.frame(width: 180, alignment: .top)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func detailContent(for tab: Tab) -> some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(tab.title)
|
||||
.font(.title2)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
switch tab {
|
||||
case .monitor:
|
||||
MonitorPreferencesView(
|
||||
pingIntervalSlider: $pingIntervalSlider,
|
||||
refreshIntervalSlider: $refreshIntervalSlider,
|
||||
showIntervalIndicator: $showIntervalIndicator,
|
||||
minimumInterval: minimumInterval,
|
||||
maximumPingInterval: maximumPingInterval,
|
||||
maximumRefreshInterval: maximumRefreshInterval,
|
||||
pingChanged: handlePingSliderEditing(_:),
|
||||
refreshChanged: handleRefreshSliderEditing(_:)
|
||||
)
|
||||
case .notifications:
|
||||
NotificationsPreferencesView()
|
||||
case .alerts:
|
||||
AlertsPreferencesView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePingSliderEditing(_ editing: Bool) {
|
||||
if !editing {
|
||||
storedPingInterval = Int(pingIntervalSlider)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleRefreshSliderEditing(_ editing: Bool) {
|
||||
if !editing {
|
||||
storedRefreshInterval = Int(refreshIntervalSlider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MonitorPreferencesView: View {
|
||||
@Binding var pingIntervalSlider: Double
|
||||
@Binding var refreshIntervalSlider: Double
|
||||
@Binding var showIntervalIndicator: Bool
|
||||
|
||||
let minimumInterval: Double
|
||||
let maximumPingInterval: Double
|
||||
let maximumRefreshInterval: Double
|
||||
let pingChanged: (Bool) -> Void
|
||||
let refreshChanged: (Bool) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
Group {
|
||||
Text("Ping interval")
|
||||
.font(.headline)
|
||||
Slider(
|
||||
value: $pingIntervalSlider,
|
||||
in: minimumInterval...maximumPingInterval,
|
||||
step: 5
|
||||
) {
|
||||
Text("Ping interval")
|
||||
} minimumValueLabel: {
|
||||
Text("\(Int(minimumInterval))s")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} maximumValueLabel: {
|
||||
Text("\(Int(maximumPingInterval))s")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} onEditingChanged: { editing in
|
||||
pingChanged(editing)
|
||||
}
|
||||
Text("Current: \(Int(pingIntervalSlider)) seconds")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Group {
|
||||
Text("Refresh interval")
|
||||
.font(.headline)
|
||||
Slider(
|
||||
value: $refreshIntervalSlider,
|
||||
in: minimumInterval...maximumRefreshInterval,
|
||||
step: 5
|
||||
) {
|
||||
Text("Refresh interval")
|
||||
} minimumValueLabel: {
|
||||
Text("\(Int(minimumInterval))s")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} maximumValueLabel: {
|
||||
Text("\(Int(maximumRefreshInterval))s")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} onEditingChanged: { editing in
|
||||
refreshChanged(editing)
|
||||
}
|
||||
Text("Current: \(Int(refreshIntervalSlider)) seconds")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Toggle("Show interval indicator", isOn: $showIntervalIndicator)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
private struct NotificationsPreferencesView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Notifications")
|
||||
.font(.headline)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Configure notification behavior here.")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
private struct AlertsPreferencesView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Alerts")
|
||||
.font(.headline)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Configure alert thresholds and behavior.")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PreferencesView()
|
||||
}
|
||||
@@ -44,6 +44,52 @@ struct GeneralView: View {
|
||||
InfoCell(value: [server.info?.formattedVersion ?? ""], monospaced: true)
|
||||
}
|
||||
|
||||
TableRowView {
|
||||
Text("Operating system")
|
||||
} value: {
|
||||
InfoCell(
|
||||
value: {
|
||||
guard let os = server.info?.operatingSystem else { return [] }
|
||||
var rows: [String] = []
|
||||
|
||||
let distro = [os.distribution, os.version]
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: " ")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var description = os.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if description.isEmpty {
|
||||
description = distro
|
||||
} else if !distro.isEmpty && distro.caseInsensitiveCompare(description) != .orderedSame {
|
||||
description += " • \(distro)"
|
||||
}
|
||||
if !os.architecture.isEmpty {
|
||||
description += " (\(os.architecture))"
|
||||
}
|
||||
if !description.isEmpty {
|
||||
rows.append(description)
|
||||
}
|
||||
|
||||
if let updates = os.updates {
|
||||
var updateDescription = "Updates: \(updates.updateCount)"
|
||||
if updates.securityUpdateCount > 0 {
|
||||
updateDescription += " • \(updates.securityUpdateCount) security"
|
||||
}
|
||||
rows.append(updateDescription)
|
||||
if updates.rebootRequired {
|
||||
rows.append("Reboot required")
|
||||
}
|
||||
}
|
||||
|
||||
if os.endOfLife {
|
||||
rows.append("End-of-life release")
|
||||
}
|
||||
|
||||
return rows
|
||||
}(),
|
||||
monospaced: true
|
||||
)
|
||||
}
|
||||
|
||||
TableRowView {
|
||||
Text("Sytem PHP version")
|
||||
} value: {
|
||||
@@ -17,5 +17,10 @@ struct iKeyMonApp: App {
|
||||
}
|
||||
}
|
||||
.windowResizability(.contentMinSize)
|
||||
|
||||
Settings {
|
||||
PreferencesView()
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,42 +7,30 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
52A9B79B2EC8E7EE004DD4A2 /* iKeyMonApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A9B7892EC8E7EE004DD4A2 /* iKeyMonApp.swift */; };
|
||||
52A9B79C2EC8E7EE004DD4A2 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A9B78A2EC8E7EE004DD4A2 /* KeychainHelper.swift */; };
|
||||
52A9B79F2EC8E7EE004DD4A2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52A9B7872EC8E7EE004DD4A2 /* Assets.xcassets */; };
|
||||
52A9B8222EC8FA8A004DD4A2 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = 52A9B8212EC8FA8A004DD4A2 /* CHANGELOG.md */; };
|
||||
52A9B8BB2ECA3605004DD4A2 /* NOTES.md in Sources */ = {isa = PBXBuildFile; fileRef = 52A9B8BA2ECA35FB004DD4A2 /* NOTES.md */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
5203C24D2D997D2800576D4A /* iKeyMon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iKeyMon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
52A9B7872EC8E7EE004DD4A2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
52A9B7882EC8E7EE004DD4A2 /* iKeyMon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iKeyMon.entitlements; sourceTree = "<group>"; };
|
||||
52A9B7892EC8E7EE004DD4A2 /* iKeyMonApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iKeyMonApp.swift; sourceTree = "<group>"; };
|
||||
52A9B78A2EC8E7EE004DD4A2 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = "<group>"; };
|
||||
52A9B8212EC8FA8A004DD4A2 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
|
||||
52A9B8BA2ECA35FB004DD4A2 /* NOTES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = NOTES.md; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
52A9B7A12EC8E84F004DD4A2 /* Extensions */ = {
|
||||
52A9B8BE2ECB68B5004DD4A2 /* Sources */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = Extensions;
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
52A9B7A72EC8E857004DD4A2 /* Model */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
52A9B7AC2EC8E85E004DD4A2 /* Preview Content */ = {
|
||||
52A9B8F72ECB6B8A004DD4A2 /* Preview Content */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
52A9B7BC2EC8E86C004DD4A2 /* Views */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -59,14 +47,11 @@
|
||||
5203C2442D997D2800576D4A = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
52A9B8BE2ECB68B5004DD4A2 /* Sources */,
|
||||
52A9B8BA2ECA35FB004DD4A2 /* NOTES.md */,
|
||||
52A9B7872EC8E7EE004DD4A2 /* Assets.xcassets */,
|
||||
52A9B7A72EC8E857004DD4A2 /* Model */,
|
||||
52A9B7882EC8E7EE004DD4A2 /* iKeyMon.entitlements */,
|
||||
52A9B7A12EC8E84F004DD4A2 /* Extensions */,
|
||||
52A9B7AC2EC8E85E004DD4A2 /* Preview Content */,
|
||||
52A9B7892EC8E7EE004DD4A2 /* iKeyMonApp.swift */,
|
||||
52A9B7BC2EC8E86C004DD4A2 /* Views */,
|
||||
52A9B78A2EC8E7EE004DD4A2 /* KeychainHelper.swift */,
|
||||
52A9B8F72ECB6B8A004DD4A2 /* Preview Content */,
|
||||
5203C24E2D997D2800576D4A /* Products */,
|
||||
52A9B8212EC8FA8A004DD4A2 /* CHANGELOG.md */,
|
||||
);
|
||||
@@ -96,10 +81,8 @@
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
52A9B7A12EC8E84F004DD4A2 /* Extensions */,
|
||||
52A9B7A72EC8E857004DD4A2 /* Model */,
|
||||
52A9B7AC2EC8E85E004DD4A2 /* Preview Content */,
|
||||
52A9B7BC2EC8E86C004DD4A2 /* Views */,
|
||||
52A9B8BE2ECB68B5004DD4A2 /* Sources */,
|
||||
52A9B8F72ECB6B8A004DD4A2 /* Preview Content */,
|
||||
);
|
||||
name = iKeyMon;
|
||||
packageProductDependencies = (
|
||||
@@ -159,8 +142,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
52A9B79B2EC8E7EE004DD4A2 /* iKeyMonApp.swift in Sources */,
|
||||
52A9B79C2EC8E7EE004DD4A2 /* KeychainHelper.swift in Sources */,
|
||||
52A9B8BB2ECA3605004DD4A2 /* NOTES.md in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user