mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-09-15 08:50:57 +00:00
parent
b308b10716
commit
f76766a9d5
@ -1,7 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
/// 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, Sendable, Hashable {
|
||||||
|
|
||||||
/// 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
|
||||||
@ -15,6 +16,8 @@ public struct Release: Codable, Sendable {
|
|||||||
/// A user-facing description of the contents of the update.
|
/// A user-facing description of the contents of the update.
|
||||||
public let body: String
|
public let body: String
|
||||||
|
|
||||||
|
public let attributedBody: AttributedString
|
||||||
|
|
||||||
/// Initializes a Release.
|
/// Initializes a Release.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - name: The user-facing name of the release.
|
/// - name: The user-facing name of the release.
|
||||||
@ -26,6 +29,56 @@ public struct Release: Codable, Sendable {
|
|||||||
self.prerelease = prerelease
|
self.prerelease = prerelease
|
||||||
self.html_url = html_url
|
self.html_url = html_url
|
||||||
self.body = body
|
self.body = body
|
||||||
|
self.attributedBody = AttributedString(_markdown: body)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(_ release: GitHubRelease) {
|
||||||
|
self.name = release.name
|
||||||
|
self.prerelease = release.prerelease
|
||||||
|
self.html_url = release.html_url
|
||||||
|
self.body = release.body
|
||||||
|
self.attributedBody = AttributedString(_markdown: release.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct GitHubRelease: Codable, Sendable {
|
||||||
|
let name: String
|
||||||
|
let prerelease: Bool
|
||||||
|
let html_url: URL
|
||||||
|
let body: String
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension AttributedString {
|
||||||
|
|
||||||
|
init(_markdown markdown: String) {
|
||||||
|
let split = markdown.split(whereSeparator: \.isNewline)
|
||||||
|
let lines = split
|
||||||
|
.compactMap {
|
||||||
|
try? AttributedString(markdown: String($0), options: .init(allowsExtendedAttributes: true, interpretedSyntax: .full))
|
||||||
|
}
|
||||||
|
.map { (string: AttributedString) in
|
||||||
|
guard case let .header(level) = string.runs.first?.presentationIntent?.components.first?.kind else { return string }
|
||||||
|
return AttributedString("\n") + string
|
||||||
|
.transformingAttributes(\.font) { font in
|
||||||
|
font.value = switch level {
|
||||||
|
case 2: .headline.bold()
|
||||||
|
case 3: .headline
|
||||||
|
default: .subheadline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transformingAttributes(\.underlineStyle) { underline in
|
||||||
|
underline.value = switch level {
|
||||||
|
case 2: .single
|
||||||
|
default: .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+ AttributedString("\n")
|
||||||
|
}
|
||||||
|
self = lines.reduce(into: AttributedString()) { partialResult, next in
|
||||||
|
partialResult.append(next)
|
||||||
|
partialResult.append(AttributedString("\n"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,9 @@ public final class XPCServiceDelegate: NSObject, NSXPCListenerDelegate {
|
|||||||
if let error = error as? Codable & Error {
|
if let error = error as? Codable & Error {
|
||||||
reply(nil, NSError(error))
|
reply(nil, NSError(error))
|
||||||
} else {
|
} else {
|
||||||
reply(nil, error)
|
// Sending cast directly tries to serialize it and crashes XPCEncoder.
|
||||||
|
let cast = error as NSError
|
||||||
|
reply(nil, NSError(domain: cast.domain, code: cast.code, userInfo: [NSLocalizedDescriptionKey: error.localizedDescription]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ extension View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MenuButtonModifier: ViewModifier {
|
struct ToolbarCircleButtonModifier: ViewModifier {
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
@ -40,8 +40,8 @@ struct MenuButtonModifier: ViewModifier {
|
|||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
|
||||||
func menuButton() -> some View {
|
func toolbarCircleButton() -> some View {
|
||||||
modifier(MenuButtonModifier())
|
modifier(ToolbarCircleButtonModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ToolbarButtonStyle: ButtonStyle {
|
struct ToolbarStatusButtonStyle: ButtonStyle {
|
||||||
|
|
||||||
private let lightColor: Color
|
private let lightColor: Color
|
||||||
private let darkColor: Color
|
private let darkColor: Color
|
||||||
@ -56,3 +56,24 @@ struct ToolbarButtonStyle: ButtonStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ToolbarButtonStyle: PrimitiveButtonStyle {
|
||||||
|
|
||||||
|
var tint: Color = .white.opacity(0.1)
|
||||||
|
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
configuration
|
||||||
|
.label
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.glassEffect(.regular.interactive().tint(tint))
|
||||||
|
} else {
|
||||||
|
BorderedButtonStyle().makeBody(configuration: configuration)
|
||||||
|
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -6,19 +6,21 @@ import Brief
|
|||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
|
||||||
@State var showingCreation = false
|
|
||||||
@State var runningSetup = false
|
|
||||||
@State var showingAgentInfo = false
|
|
||||||
@State var activeSecret: AnySecret?
|
@State var activeSecret: AnySecret?
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
|
|
||||||
@Environment(\.secretStoreList) private var storeList
|
|
||||||
@Environment(\.updater) private var updater: any UpdaterProtocol
|
|
||||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
|
||||||
|
|
||||||
@State private var selectedUpdate: Release?
|
@State private var selectedUpdate: Release?
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.openWindow) private var openWindow
|
||||||
|
@Environment(\.secretStoreList) private var storeList
|
||||||
|
@Environment(\.updater) private var updater
|
||||||
|
@Environment(\.agentStatusChecker) private var agentStatusChecker
|
||||||
|
|
||||||
|
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
|
||||||
|
@State private var showingCreation = false
|
||||||
@State private var showingAppPathNotice = false
|
@State private var showingAppPathNotice = false
|
||||||
|
@State private var runningSetup = false
|
||||||
|
@State private var showingAgentInfo = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
@ -102,24 +104,9 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.buttonStyle(ToolbarButtonStyle(color: color))
|
.buttonStyle(ToolbarStatusButtonStyle(color: color))
|
||||||
.sheet(item: $selectedUpdate) { update in
|
.sheet(item: $selectedUpdate) { update in
|
||||||
VStack {
|
UpdateDetailView(update: update)
|
||||||
if updater.currentVersion.isTestBuild {
|
|
||||||
VStack {
|
|
||||||
if let description = updater.currentVersion.previewDescription {
|
|
||||||
Text(description)
|
|
||||||
}
|
|
||||||
Link(destination: URL(string: "https://github.com/maxgoedjen/secretive/actions/workflows/nightly.yml")!) {
|
|
||||||
Button(.updaterDownloadLatestNightlyButton) {}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.primaryButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
UpdateDetailView(update: update)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,7 +117,7 @@ extension ContentView {
|
|||||||
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
||||||
showingCreation = true
|
showingCreation = true
|
||||||
}
|
}
|
||||||
.menuButton()
|
.toolbarCircleButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +144,7 @@ extension ContentView {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.buttonStyle(
|
.buttonStyle(
|
||||||
ToolbarButtonStyle(
|
ToolbarStatusButtonStyle(
|
||||||
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
||||||
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
||||||
)
|
)
|
||||||
@ -179,7 +166,7 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
.buttonStyle(ToolbarStatusButtonStyle(color: .orange))
|
||||||
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
||||||
VStack {
|
VStack {
|
||||||
Image(systemName: "exclamationmark.triangle")
|
Image(systemName: "exclamationmark.triangle")
|
||||||
|
@ -3,61 +3,50 @@ import Brief
|
|||||||
|
|
||||||
struct UpdateDetailView: View {
|
struct UpdateDetailView: View {
|
||||||
|
|
||||||
@Environment(\.updater) var updater: any UpdaterProtocol
|
@Environment(\.updater) var updater
|
||||||
|
@Environment(\.openURL) var openURL
|
||||||
|
|
||||||
let update: Release
|
let update: Release
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack(spacing: 0) {
|
||||||
Text(.updateVersionName(updateName: update.name)).font(.title)
|
HStack {
|
||||||
GroupBox(label: Text(.updateReleaseNotesTitle)) {
|
if !update.critical {
|
||||||
ScrollView {
|
Button(.updateIgnoreButton) {
|
||||||
attributedBody
|
Task {
|
||||||
}
|
await updater.ignore(release: update)
|
||||||
}
|
}
|
||||||
HStack {
|
|
||||||
if !update.critical {
|
|
||||||
Button(.updateIgnoreButton) {
|
|
||||||
Task {
|
|
||||||
await updater.ignore(release: update)
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(ToolbarButtonStyle())
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
if updater.currentVersion.isTestBuild {
|
||||||
|
Button(.updaterDownloadLatestNightlyButton) {
|
||||||
|
openURL(URL(string: "https://github.com/maxgoedjen/secretive/actions/workflows/nightly.yml")!)
|
||||||
|
}
|
||||||
|
.buttonStyle(ToolbarButtonStyle(tint: .accentColor))
|
||||||
|
}
|
||||||
|
Button(.updateUpdateButton) {
|
||||||
|
openURL(update.html_url)
|
||||||
|
}
|
||||||
|
.buttonStyle(ToolbarButtonStyle(tint: .accentColor))
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
}
|
}
|
||||||
Button(.updateUpdateButton) {
|
.padding()
|
||||||
NSWorkspace.shared.open(update.html_url)
|
Divider()
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
Text(update.attributedBody)
|
||||||
|
} header: {
|
||||||
|
Text(.updateVersionName(updateName: update.name)) .headerProminence(.increased)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.defaultAction)
|
.formStyle(.grouped)
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
.frame(maxWidth: 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
var attributedBody: Text {
|
|
||||||
var text = Text(verbatim: "")
|
|
||||||
for line in update.body.split(whereSeparator: \.isNewline) {
|
|
||||||
let attributed: Text
|
|
||||||
let split = line.split(separator: " ")
|
|
||||||
let unprefixed = split.dropFirst().joined(separator: " ")
|
|
||||||
if let prefix = split.first {
|
|
||||||
switch prefix {
|
|
||||||
case "#":
|
|
||||||
attributed = Text(unprefixed).font(.title) + Text(verbatim: "\n")
|
|
||||||
case "##":
|
|
||||||
attributed = Text(unprefixed).font(.title2) + Text(verbatim: "\n")
|
|
||||||
case "###":
|
|
||||||
attributed = Text(unprefixed).font(.title3) + Text(verbatim: "\n")
|
|
||||||
default:
|
|
||||||
attributed = Text(line) + Text(verbatim: "\n\n")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
attributed = Text(line) + Text(verbatim: "\n\n")
|
|
||||||
}
|
|
||||||
text = text + attributed
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
UpdateDetailView(update: .init(name: "3.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Hello"))
|
||||||
|
}
|
||||||
|
@ -11,7 +11,9 @@ final class SecretiveUpdater: NSObject, XPCProtocol {
|
|||||||
|
|
||||||
func process(_: Data) async throws -> [Release] {
|
func process(_: Data) async throws -> [Release] {
|
||||||
let (data, _) = try await URLSession.shared.data(from: Constants.updateURL)
|
let (data, _) = try await URLSession.shared.data(from: Constants.updateURL)
|
||||||
return try JSONDecoder().decode([Release].self, from: data)
|
return try JSONDecoder()
|
||||||
|
.decode([GitHubRelease].self, from: data)
|
||||||
|
.map(Release.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user