Compare commits

..

10 Commits

16 changed files with 169 additions and 113 deletions

49
.github/workflows/nightly.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: Nightly
on:
schedule:
- cron: "0 8 * * *"
jobs:
build:
runs-on: macos-11.0
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- name: Setup Signing
env:
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: ./.github/scripts/signing.sh
- name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_13.2.1.app
- name: Update Build Number
env:
RUN_ID: ${{ github.run_id }}
run: |
sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0/g" Sources/Config/Config.xcconfig
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
- name: Build
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
- name: Create ZIPs
run: |
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
- name: Notarize
env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
- name: Document SHAs
run: |
shasum -a 512 Secretive.zip
shasum -a 512 Archive.zip
- name: Upload App to Artifacts
uses: actions/upload-artifact@v1
with:
name: Secretive.zip
path: Secretive.zip

View File

@ -51,6 +51,30 @@ Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
Log out and log in again before launching Cyberduck. Log out and log in again before launching Cyberduck.
## GitKraken
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>link-ssh-auth-sock</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>/bin/ln -sf $HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh $SSH_AUTH_SOCK</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
```
Log out and log in again before launching Gitkraken. Then enable "Use local SSH agent in GitKraken Preferences (Located under Preferences -> SSH)
# The app I use isn't listed here! # The app I use isn't listed here!

4
FAQ.md
View File

@ -40,6 +40,10 @@ Awesome! Just bear in mind that because an app only has access to the keychain i
Secretive checks in with GitHub's releases API to check if there's a new version of Secretive available. You can audit the source code for this feature [here](https://github.com/maxgoedjen/secretive/blob/main/Sources/Packages/Sources/Brief/Updater.swift). Secretive checks in with GitHub's releases API to check if there's a new version of Secretive available. You can audit the source code for this feature [here](https://github.com/maxgoedjen/secretive/blob/main/Sources/Packages/Sources/Brief/Updater.swift).
### How do I uninstall Secretive?
Drag Secretive.app to the trash and remove `~/Library/Containers/com.maxgoedjen.Secretive.SecretAgent`. `SecretAgent` may continue running until you quit it or reboot.
### I have a security issue ### I have a security issue
Please contact [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with a subject containing "SECRETIVE SECURITY" immediately with details, and I'll address the issue and credit you ASAP. Please contact [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with a subject containing "SECRETIVE SECURITY" immediately with details, and I'll address the issue and credit you ASAP.

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// A release is a representation of a downloadable update. /// A release is a representation of a downloadable update.
public struct Release: Codable, Sendable { public struct Release: Codable {
/// The user-facing name of the release. Typically "Secretive 1.2.3" /// The user-facing name of the release. Typically "Secretive 1.2.3"
public let name: String public let name: String
@ -30,9 +30,6 @@ public struct Release: Codable, Sendable {
} }
// TODO: REMOVE WHEN(?) URL GAINS NATIVE CONFORMANCE
extension URL: @unchecked Sendable {}
extension Release: Identifiable { extension Release: Identifiable {
public var id: String { public var id: String {

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// A representation of a Semantic Version. /// A representation of a Semantic Version.
public struct SemVer: Sendable { public struct SemVer {
/// The SemVer broken into an array of integers. /// The SemVer broken into an array of integers.
let versionNumbers: [Int] let versionNumbers: [Int]

View File

@ -2,59 +2,53 @@ import Foundation
import Combine import Combine
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version. /// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
public actor Updater: ObservableObject, UpdaterProtocol { public class Updater: ObservableObject, UpdaterProtocol {
@MainActor @Published public var update: Release? @Published public var update: Release?
public let testBuild: Bool public let testBuild: Bool
/// The current OS version. /// The current OS version.
private let osVersion: SemVer private let osVersion: SemVer
/// The current version of the app that is running. /// The current version of the app that is running.
private let currentVersion: SemVer private let currentVersion: SemVer
/// The timer responsible for checking for updates regularly.
private var timer: Timer? = nil
/// Initializes an Updater. /// Initializes an Updater.
/// - Parameters: /// - Parameters:
/// - checkOnLaunch: A boolean describing whether the Updater should check for available updates on launch.
/// - checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour.
/// - osVersion: The current OS version. /// - osVersion: The current OS version.
/// - currentVersion: The current version of the app that is running. /// - currentVersion: The current version of the app that is running.
public init(osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) { public init(checkOnLaunch: Bool, checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) {
self.osVersion = osVersion self.osVersion = osVersion
self.currentVersion = currentVersion self.currentVersion = currentVersion
testBuild = currentVersion == SemVer("0.0.0") testBuild = currentVersion == SemVer("0.0.0")
} if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
/// Begins checking for updates with the specified frequency. checkForUpdates()
/// - Parameter checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour.
public func beginChecking(checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in
Task {
await self.checkForUpdates()
}
} }
timer?.tolerance = 60*60 let timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in
} self.checkForUpdates()
}
/// Ends checking for updates. timer.tolerance = 60*60
public func stopChecking() {
timer?.invalidate()
timer = nil
} }
/// Manually trigger an update check. /// Manually trigger an update check.
public func checkForUpdates() async { public func checkForUpdates() {
guard let (data, _) = try? await URLSession.shared.data(from: Constants.updateURL), URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
let releases = try? JSONDecoder().decode([Release].self, from: data) else { return } guard let data = data else { return }
await evaluate(releases: releases) guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
self.evaluate(releases: releases)
}.resume()
} }
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release. /// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.
/// - Parameter release: The release to ignore. /// - Parameter release: The release to ignore.
public func ignore(release: Release) async { public func ignore(release: Release) {
guard !release.critical else { return } guard !release.critical else { return }
defaults.set(true, forKey: release.name) defaults.set(true, forKey: release.name)
await setUpdate(update: update) DispatchQueue.main.async {
self.update = nil
}
} }
} }
@ -63,7 +57,7 @@ extension Updater {
/// Evaluates the available downloadable releases, and selects the newest non-prerelease release that the user is able to run. /// Evaluates the available downloadable releases, and selects the newest non-prerelease release that the user is able to run.
/// - Parameter releases: An array of ``Release`` objects. /// - Parameter releases: An array of ``Release`` objects.
func evaluate(releases: [Release]) async { func evaluate(releases: [Release]) {
guard let release = releases guard let release = releases
.sorted() .sorted()
.reversed() .reversed()
@ -73,14 +67,12 @@ extension Updater {
guard !release.prerelease else { return } guard !release.prerelease else { return }
let latestVersion = SemVer(release.name) let latestVersion = SemVer(release.name)
if latestVersion > currentVersion { if latestVersion > currentVersion {
await setUpdate(update: update) DispatchQueue.main.async {
self.update = release
}
} }
} }
@MainActor private func setUpdate(update: Release?) {
self.update = update
}
/// Checks whether the user has ignored a release. /// Checks whether the user has ignored a release.
/// - Parameter release: The release to check. /// - Parameter release: The release to check.
/// - Returns: A boolean describing whether the user has ignored the release. Will always be false if the release is critical. /// - Returns: A boolean describing whether the user has ignored the release. Will always be false if the release is critical.
@ -103,21 +95,3 @@ extension Updater {
} }
} }
@available(macOS, deprecated: 12)
extension URLSession {
// Backport for macOS 11
func data(from url: URL) async throws -> (Data, URLResponse) {
try await withCheckedThrowingContinuation { continuation in
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, let response = response else {
continuation.resume(throwing: error ?? NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil))
return
}
continuation.resume(returning: (data, response))
}
}
}
}

View File

@ -4,9 +4,9 @@ import Foundation
public protocol UpdaterProtocol: ObservableObject { public protocol UpdaterProtocol: ObservableObject {
/// The latest update /// The latest update
@MainActor var update: Release? { get } var update: Release? { get }
/// A boolean describing whether or not the current build of the app is a "test" build (ie, a debug build or otherwise special build) /// A boolean describing whether or not the current build of the app is a "test" build (ie, a debug build or otherwise special build)
@MainActor var testBuild: Bool { get } var testBuild: Bool { get }
} }

View File

@ -30,20 +30,23 @@ extension Agent {
/// - Parameters: /// - Parameters:
/// - reader: A ``FileHandleReader`` to read the content of the request. /// - reader: A ``FileHandleReader`` to read the content of the request.
/// - writer: A ``FileHandleWriter`` to write the response to. /// - writer: A ``FileHandleWriter`` to write the response to.
public func handle(reader: FileHandleReader, writer: FileHandleWriter) { /// - Return value:
/// - Boolean if data could be read
public func handle(reader: FileHandleReader, writer: FileHandleWriter) -> Bool {
Logger().debug("Agent handling new data") Logger().debug("Agent handling new data")
let data = Data(reader.availableData) let data = Data(reader.availableData)
guard data.count > 4 else { return } guard data.count > 4 else { return false}
let requestTypeInt = data[4] let requestTypeInt = data[4]
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else { guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data)) writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)") Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
return return true
} }
Logger().debug("Agent handling request of type \(requestType.debugDescription)") Logger().debug("Agent handling request of type \(requestType.debugDescription)")
let subData = Data(data[5...]) let subData = Data(data[5...])
let response = handle(requestType: requestType, data: subData, reader: reader) let response = handle(requestType: requestType, data: subData, reader: reader)
writer.write(response) writer.write(response)
return true
} }
func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data { func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data {

View File

@ -9,7 +9,9 @@ public class SocketController {
/// The active SocketPort. /// The active SocketPort.
private var port: SocketPort? private var port: SocketPort?
/// A handler that will be notified when a new read/write handle is available. /// A handler that will be notified when a new read/write handle is available.
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)? /// False if no data could be read
public var handler: ((FileHandleReader, FileHandleWriter) -> Bool)?
/// Initializes a socket controller with a specified path. /// Initializes a socket controller with a specified path.
/// - Parameter path: The path to use as a socket. /// - Parameter path: The path to use as a socket.
@ -65,7 +67,7 @@ public class SocketController {
@objc func handleConnectionAccept(notification: Notification) { @objc func handleConnectionAccept(notification: Notification) {
Logger().debug("Socket controller accepted connection") Logger().debug("Socket controller accepted connection")
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return } guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
handler?(new, new) _ = handler?(new, new)
new.waitForDataInBackgroundAndNotify() new.waitForDataInBackgroundAndNotify()
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!]) fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
} }
@ -76,7 +78,12 @@ public class SocketController {
Logger().debug("Socket controller has new data available") Logger().debug("Socket controller has new data available")
guard let new = notification.object as? FileHandle else { return } guard let new = notification.object as? FileHandle else { return }
Logger().debug("Socket controller received new file handle") Logger().debug("Socket controller received new file handle")
handler?(new, new) if((handler?(new, new)) == true) {
Logger().debug("Socket controller handled data, wait for more data")
new.waitForDataInBackgroundAndNotify()
} else {
Logger().debug("Socket controller called with empty data, socked closed")
}
} }
} }

View File

@ -6,6 +6,7 @@ public class PublicKeyFileStoreController {
private let logger = Logger() private let logger = Logger()
private let directory: String private let directory: String
private let keyWriter = OpenSSHKeyWriter()
/// Initializes a PublicKeyFileStoreController. /// Initializes a PublicKeyFileStoreController.
public init(homeDirectory: String) { public init(homeDirectory: String) {
@ -21,7 +22,6 @@ public class PublicKeyFileStoreController {
try? FileManager.default.removeItem(at: URL(fileURLWithPath: directory)) try? FileManager.default.removeItem(at: URL(fileURLWithPath: directory))
} }
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil) try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
let keyWriter = OpenSSHKeyWriter()
for secret in secrets { for secret in secrets {
let path = path(for: secret) let path = path(for: secret)
guard let data = keyWriter.openSSHString(secret: secret).data(using: .utf8) else { continue } guard let data = keyWriter.openSSHString(secret: secret).data(using: .utf8) else { continue }
@ -35,7 +35,8 @@ public class PublicKeyFileStoreController {
/// - Returns: The path to the Secret's public key. /// - Returns: The path to the Secret's public key.
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to. /// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
public func path<SecretType: Secret>(for secret: SecretType) -> String { public func path<SecretType: Secret>(for secret: SecretType) -> String {
directory.appending("/").appending("\(secret.name.replacingOccurrences(of: " ", with: "-")).pub") let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return directory.appending("/").appending("\(minimalHex).pub")
} }
} }

View File

@ -56,6 +56,9 @@ public protocol SecretStoreModifiable: SecretStore {
extension NSNotification.Name { extension NSNotification.Name {
// Distributed notification that keys were modified out of process (ie, that the management tool added/removed secrets)
public static let secretStoreUpdated = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.updated") public static let secretStoreUpdated = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.updated")
// Internal notification that keys were reloaded from the backing store.
public static let secretStoreReloaded = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.reloaded")
} }

View File

@ -24,7 +24,7 @@ extension SecureEnclave {
/// Initializes a Store. /// Initializes a Store.
public init() { public init() {
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
self.reloadSecrets(notify: false) self.reloadSecrets(notifyAgent: false)
} }
loadSecrets() loadSecrets()
} }
@ -171,12 +171,13 @@ extension SecureEnclave {
extension SecureEnclave.Store { extension SecureEnclave.Store {
/// Reloads all secrets from the store. /// Reloads all secrets from the store.
/// - Parameter notify: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well. /// - Parameter notifyAgent: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well.
private func reloadSecrets(notify: Bool = true) { private func reloadSecrets(notifyAgent: Bool = true) {
secrets.removeAll() secrets.removeAll()
loadSecrets() loadSecrets()
if notify { NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
DistributedNotificationCenter.default().post(name: .secretStoreUpdated, object: nil) if notifyAgent {
DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true)
} }
} }

View File

@ -16,7 +16,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
list.add(store: SmartCard.Store()) list.add(store: SmartCard.Store())
return list return list
}() }()
private let updater = Updater() private let updater = Updater(checkOnLaunch: false)
private let notifier = Notifier() private let notifier = Notifier()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
private lazy var agent: Agent = { private lazy var agent: Agent = {
@ -33,15 +33,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
DispatchQueue.main.async { DispatchQueue.main.async {
self.socketController.handler = self.agent.handle(reader:writer:) self.socketController.handler = self.agent.handle(reader:writer:)
} }
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { [self] _ in NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true) try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
} }
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true) try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
notifier.prompt() notifier.prompt()
// updateSink = updater.$update.sink { update in updateSink = updater.$update.sink { update in
// guard let update = update else { return } guard let update = update else { return }
// self.notifier.notify(update: update, ignore: self.updater.ignore(release:)) self.notifier.notify(update: update, ignore: self.updater.ignore(release:))
// } }
} }
} }

View File

@ -16,7 +16,6 @@ struct Secretive: App {
}() }()
private let agentStatusChecker = AgentStatusChecker() private let agentStatusChecker = AgentStatusChecker()
private let justUpdatedChecker = JustUpdatedChecker() private let justUpdatedChecker = JustUpdatedChecker()
private let updater = Updater()
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@State private var showingSetup = false @State private var showingSetup = false
@ -26,15 +25,11 @@ struct Secretive: App {
WindowGroup { WindowGroup {
ContentView<Updater, AgentStatusChecker>(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup) ContentView<Updater, AgentStatusChecker>(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
.environmentObject(storeList) .environmentObject(storeList)
.environmentObject(updater) .environmentObject(Updater(checkOnLaunch: hasRunSetup))
.environmentObject(agentStatusChecker) .environmentObject(agentStatusChecker)
.onAppear { .onAppear {
if !hasRunSetup { if !hasRunSetup {
showingSetup = true showingSetup = true
} else {
Task { [updater] in
await updater.checkForUpdates()
}
} }
} }
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in

View File

@ -32,6 +32,9 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
appPathNotice appPathNotice
newItem newItem
} }
.sheet(isPresented: $runningSetup) {
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
}
} }
} }
@ -65,11 +68,11 @@ extension ContentView {
.font(.headline) .font(.headline)
.foregroundColor(.white) .foregroundColor(.white)
}) })
.background(color) .background(color)
.cornerRadius(5) .cornerRadius(5)
.popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in .popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in
UpdateDetailView(update: update) UpdateDetailView(update: update)
} }
) )
} }
} }
@ -85,11 +88,11 @@ extension ContentView {
}, label: { }, label: {
Image(systemName: "plus") Image(systemName: "plus")
}) })
.popover(isPresented: $showingCreation, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { .popover(isPresented: $showingCreation, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
if let modifiable = storeList.modifiableStore { if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable, showing: $showingCreation) CreateSecretView(store: modifiable, showing: $showingCreation)
}
} }
}
) )
} }
@ -113,15 +116,12 @@ extension ContentView {
.font(.headline) .font(.headline)
.foregroundColor(.white) .foregroundColor(.white)
}) })
.background(Color.orange) .background(Color.orange)
.cornerRadius(5) .cornerRadius(5)
} else { } else {
EmptyView() EmptyView()
} }
} }
.sheet(isPresented: $runningSetup) {
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
}
) )
} }
} }
@ -142,19 +142,19 @@ extension ContentView {
.font(.headline) .font(.headline)
.foregroundColor(.white) .foregroundColor(.white)
}) })
.background(Color.orange) .background(Color.orange)
.cornerRadius(5) .cornerRadius(5)
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { .popover(isPresented: $showingAppPathNotice, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
VStack { VStack {
Image(systemName: "exclamationmark.triangle") Image(systemName: "exclamationmark.triangle")
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 64) .frame(width: 64)
Text("Secretive needs to be in your Applications folder to work properly. Please move it and relaunch.") Text("Secretive needs to be in your Applications folder to work properly. Please move it and relaunch.")
.frame(maxWidth: 300) .frame(maxWidth: 300)
}
.padding()
} }
.padding()
}
) )
} }
} }

View File

@ -18,9 +18,7 @@ struct UpdateDetailView<UpdaterType: Updater>: View {
HStack { HStack {
if !update.critical { if !update.critical {
Button("Ignore") { Button("Ignore") {
Task { [updater, update] in updater.ignore(release: update)
await updater.ignore(release: update)
}
} }
Spacer() Spacer()
} }