mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-09-19 14:48:01 +02:00
Merge branch 'main' into auth_splitout
This commit is contained in:
@@ -22,6 +22,9 @@ let package = Package(
|
|||||||
.library(
|
.library(
|
||||||
name: "CertificateKit",
|
name: "CertificateKit",
|
||||||
targets: ["CertificateKit"]),
|
targets: ["CertificateKit"]),
|
||||||
|
.library(
|
||||||
|
name: "SettingsKit",
|
||||||
|
targets: ["SettingsKit"]),
|
||||||
.library(
|
.library(
|
||||||
name: "SecretAgentKit",
|
name: "SecretAgentKit",
|
||||||
targets: ["SecretAgentKit"]),
|
targets: ["SecretAgentKit"]),
|
||||||
@@ -76,6 +79,12 @@ let package = Package(
|
|||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "SettingsKit",
|
||||||
|
dependencies: [],
|
||||||
|
resources: [localization],
|
||||||
|
swiftSettings: swiftSettings,
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SecretAgentKit",
|
name: "SecretAgentKit",
|
||||||
dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common", "Formatters"],
|
dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common", "Formatters"],
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import SecretKit
|
|||||||
public func update(certificate: Certificate) throws {
|
public func update(certificate: Certificate) throws {
|
||||||
let updateQuery = KeychainDictionary([
|
let updateQuery = KeychainDictionary([
|
||||||
kSecClass: Constants.keyClass,
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
kSecAttrAccount: certificate.id,
|
kSecAttrAccount: certificate.id,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ extension CertificateStore {
|
|||||||
|
|
||||||
enum Constants {
|
enum Constants {
|
||||||
static let keyClass = kSecClassGenericPassword as String
|
static let keyClass = kSecClassGenericPassword as String
|
||||||
static let keyTag = Data("com.maxgoedjen.certificatestore.opensshcertificate".utf8)
|
static let keyTag = "com.maxgoedjen.certificatestore.opensshcertificate"
|
||||||
static let notificationToken = UUID().uuidString
|
static let notificationToken = UUID().uuidString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ extension SecureEnclave {
|
|||||||
let attributes = try JSONEncoder().encode(attributes)
|
let attributes = try JSONEncoder().encode(attributes)
|
||||||
let updatedAttributes = KeychainDictionary([
|
let updatedAttributes = KeychainDictionary([
|
||||||
kSecAttrLabel: name,
|
kSecAttrLabel: name,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
kSecAttrGeneric: attributes,
|
kSecAttrGeneric: attributes,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -268,7 +269,7 @@ extension SecureEnclave.Store {
|
|||||||
|
|
||||||
enum Constants {
|
enum Constants {
|
||||||
static let keyClass = kSecClassGenericPassword as String
|
static let keyClass = kSecClassGenericPassword as String
|
||||||
static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8)
|
static let keyTag = "com.maxgoedjen.secretive.secureenclave.key"
|
||||||
static let notificationToken = UUID().uuidString
|
static let notificationToken = UUID().uuidString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct RequireDestinationInformationSettingsKey: SettingsStore.SettingsKey {
|
||||||
|
static let defaultValue: Bool = true
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SettingsStore {
|
||||||
|
|
||||||
|
public var requireDestinationInformation: Bool {
|
||||||
|
get { self[RequireDestinationInformationSettingsKey.self] }
|
||||||
|
set { self[RequireDestinationInformationSettingsKey.self] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import Foundation
|
||||||
|
import Observation
|
||||||
|
import Security
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
// Setting store backed by macOS keychain for stronger guarantees around ownership/other-process-modification than UserDefaults offers.
|
||||||
|
@Observable @MainActor public final class SettingsStore: Sendable {
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.settings", category: "SettingsStore")
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private var state: [ObjectIdentifier: UUID] = [:]
|
||||||
|
|
||||||
|
subscript<SettingsKeyType: SettingsKey>(_ key: SettingsKeyType.Type) -> SettingsKeyType.Value {
|
||||||
|
get {
|
||||||
|
_ = state[ObjectIdentifier(key)]
|
||||||
|
let queryAttributes = KeychainDictionary([
|
||||||
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
|
kSecAttrAccount: String(describing: SettingsKeyType.self),
|
||||||
|
kSecUseDataProtectionKeychain: true,
|
||||||
|
kSecReturnData: true,
|
||||||
|
kSecReturnAttributes: true,
|
||||||
|
kSecMatchLimit: kSecMatchLimitOne,
|
||||||
|
])
|
||||||
|
var untyped: CFTypeRef?
|
||||||
|
unsafe SecItemCopyMatching(queryAttributes, &untyped)
|
||||||
|
guard let typed = untyped as? [CFString: Any] else { return SettingsKeyType.defaultValue }
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
guard let data = typed[kSecValueData] as? Data else { return SettingsKeyType.defaultValue }
|
||||||
|
return (try? decoder.decode(SettingValue<SettingsKeyType.Value>.self, from: data).value) ?? SettingsKeyType.defaultValue
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
do {
|
||||||
|
let data = try JSONEncoder().encode(SettingValue(value: newValue))
|
||||||
|
let keychainAttributes = KeychainDictionary([
|
||||||
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
|
kSecAttrAccount: String(describing: SettingsKeyType.self),
|
||||||
|
kSecUseDataProtectionKeychain: true,
|
||||||
|
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||||
|
kSecValueData: data,
|
||||||
|
])
|
||||||
|
let status = SecItemAdd(keychainAttributes, nil)
|
||||||
|
switch status {
|
||||||
|
case errSecSuccess:
|
||||||
|
break
|
||||||
|
case errSecDuplicateItem:
|
||||||
|
let updateQuery = KeychainDictionary([
|
||||||
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
|
kSecAttrAccount: String(describing: SettingsKeyType.self),
|
||||||
|
])
|
||||||
|
let updatedAttributes = KeychainDictionary([
|
||||||
|
kSecValueData: data,
|
||||||
|
])
|
||||||
|
let status = SecItemUpdate(updateQuery, updatedAttributes)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
state[ObjectIdentifier(key)] = UUID()
|
||||||
|
} catch {
|
||||||
|
logger.error("Error updating key: \(String(describing: SettingsKeyType.self), privacy: .public): \(error.localizedDescription.debugDescription, privacy: .public)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SettingsStore {
|
||||||
|
|
||||||
|
public protocol SettingsKey<Value>: Equatable, Codable, Sendable {
|
||||||
|
associatedtype Value: Codable
|
||||||
|
static var defaultValue: Value { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SettingsStore {
|
||||||
|
|
||||||
|
struct SettingValue<Value: Codable>: Codable {
|
||||||
|
let value: Value
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension SettingsStore {
|
||||||
|
|
||||||
|
fileprivate struct KeychainError: Error {
|
||||||
|
let statusCode: OSStatus?
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func KeychainDictionary(_ dictionary: [CFString: Any]) -> CFDictionary {
|
||||||
|
dictionary as CFDictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension SettingsStore {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let keyClass = kSecClassGenericPassword as String
|
||||||
|
static let keyTag = "com.maxgoedjen.settingsStore"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
||||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
||||||
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; };
|
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; };
|
||||||
|
5018D826304F6F1600F1FEE8 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018D825304F6F1600F1FEE8 /* SettingsView.swift */; };
|
||||||
|
5018D828304F6F4200F1FEE8 /* SettingsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5018D827304F6F4200F1FEE8 /* SettingsKit */; };
|
||||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
||||||
503647482F870B7800977A23 /* BatchedRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503647472F870B7800977A23 /* BatchedRequestsView.swift */; };
|
503647482F870B7800977A23 /* BatchedRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503647472F870B7800977A23 /* BatchedRequestsView.swift */; };
|
||||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
||||||
@@ -236,6 +238,7 @@
|
|||||||
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
||||||
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
||||||
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
|
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
|
||||||
|
5018D825304F6F1600F1FEE8 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||||
503647472F870B7800977A23 /* BatchedRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchedRequestsView.swift; sourceTree = "<group>"; };
|
503647472F870B7800977A23 /* BatchedRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchedRequestsView.swift; sourceTree = "<group>"; };
|
||||||
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
|
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
|
||||||
@@ -322,6 +325,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */,
|
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */,
|
||||||
|
5018D828304F6F4200F1FEE8 /* SettingsKit in Frameworks */,
|
||||||
50E2058A2FAC2EB600402380 /* Formatters in Frameworks */,
|
50E2058A2FAC2EB600402380 /* Formatters in Frameworks */,
|
||||||
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
|
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
|
||||||
501421622781262300BBAA70 /* Brief in Frameworks */,
|
501421622781262300BBAA70 /* Brief in Frameworks */,
|
||||||
@@ -404,6 +408,7 @@
|
|||||||
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
||||||
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */,
|
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */,
|
||||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
||||||
|
5018D825304F6F1600F1FEE8 /* SettingsView.swift */,
|
||||||
);
|
);
|
||||||
path = Secrets;
|
path = Secrets;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -653,6 +658,7 @@
|
|||||||
505F5EF12FA9635700C45824 /* CertificateKit */,
|
505F5EF12FA9635700C45824 /* CertificateKit */,
|
||||||
50E205832FAB296A00402380 /* SharedXPCServices */,
|
50E205832FAB296A00402380 /* SharedXPCServices */,
|
||||||
50E205892FAC2EB600402380 /* Formatters */,
|
50E205892FAC2EB600402380 /* Formatters */,
|
||||||
|
5018D827304F6F4200F1FEE8 /* SettingsKit */,
|
||||||
);
|
);
|
||||||
productName = Secretive;
|
productName = Secretive;
|
||||||
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
|
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
|
||||||
@@ -772,7 +778,7 @@
|
|||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 2700;
|
LastSwiftUpdateCheck = 2700;
|
||||||
LastUpgradeCheck = 2640;
|
LastUpgradeCheck = 2700;
|
||||||
ORGANIZATIONNAME = "Max Goedjen";
|
ORGANIZATIONNAME = "Max Goedjen";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
5054028C3034B5E3000C3356 = {
|
5054028C3034B5E3000C3356 = {
|
||||||
@@ -915,6 +921,7 @@
|
|||||||
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
|
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
|
||||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
|
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
|
||||||
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
|
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
|
||||||
|
5018D826304F6F1600F1FEE8 /* SettingsView.swift in Sources */,
|
||||||
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
|
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
|
||||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
|
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
|
||||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
||||||
@@ -2092,6 +2099,10 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Brief;
|
productName = Brief;
|
||||||
};
|
};
|
||||||
|
5018D827304F6F4200F1FEE8 /* SettingsKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = SettingsKit;
|
||||||
|
};
|
||||||
505402A63034B7A4000C3356 /* XPCWrappers */ = {
|
505402A63034B7A4000C3356 /* XPCWrappers */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = XPCWrappers;
|
productName = XPCWrappers;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import SecureEnclaveSecretKit
|
|||||||
import SmartCardSecretKit
|
import SmartCardSecretKit
|
||||||
import Brief
|
import Brief
|
||||||
import CertificateKit
|
import CertificateKit
|
||||||
|
import SettingsKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Secretive: App {
|
struct Secretive: App {
|
||||||
@@ -16,6 +17,7 @@ struct Secretive: App {
|
|||||||
ContentView()
|
ContentView()
|
||||||
.environment(EnvironmentValues._secretStoreList)
|
.environment(EnvironmentValues._secretStoreList)
|
||||||
.environment(EnvironmentValues._certificateStore)
|
.environment(EnvironmentValues._certificateStore)
|
||||||
|
.environment(EnvironmentValues._settingsStore)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||||
Task {
|
Task {
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||||
@@ -42,6 +44,9 @@ struct Secretive: App {
|
|||||||
}
|
}
|
||||||
.windowStyle(.hiddenTitleBar)
|
.windowStyle(.hiddenTitleBar)
|
||||||
.windowResizability(.contentSize)
|
.windowResizability(.contentSize)
|
||||||
|
Settings {
|
||||||
|
SettingsView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -101,7 +106,8 @@ extension EnvironmentValues {
|
|||||||
return list
|
return list
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
|
@MainActor fileprivate static let _certificateStore = CertificateStore()
|
||||||
|
@MainActor fileprivate static let _settingsStore = SettingsStore()
|
||||||
|
|
||||||
private static let _agentLaunchController = AgentLaunchController()
|
private static let _agentLaunchController = AgentLaunchController()
|
||||||
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
||||||
@@ -122,6 +128,10 @@ extension EnvironmentValues {
|
|||||||
@MainActor var certificateStore: CertificateStore {
|
@MainActor var certificateStore: CertificateStore {
|
||||||
EnvironmentValues._certificateStore
|
EnvironmentValues._certificateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor var settingsStore: SettingsStore {
|
||||||
|
EnvironmentValues._settingsStore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FocusedValues {
|
extension FocusedValues {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SettingsKit
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
|
||||||
|
@Environment(\.settingsStore) var settingsStore
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(minWidth: 480, minHeight: 320)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user