From b7380bea3506728419b05e081a927d380ce8fc0d Mon Sep 17 00:00:00 2001 From: NimBold Date: Tue, 9 Jun 2026 13:40:07 +0330 Subject: [PATCH] feat(settings): redesign integration pane and fix toast notification bugs --- Extensions/Firefox | 2 +- .../Firelink/Settings/AboutSettingsPane.swift | 20 -- .../Settings/IntegrationSettingsPane.swift | 262 +++++++++--------- .../Settings/KeychainAccessCard.swift | 95 +++---- .../Firelink/Settings/ToastNotification.swift | 9 +- 5 files changed, 187 insertions(+), 201 deletions(-) diff --git a/Extensions/Firefox b/Extensions/Firefox index 1636855..fc078ef 160000 --- a/Extensions/Firefox +++ b/Extensions/Firefox @@ -1 +1 @@ -Subproject commit 1636855c3faef2910d220306e9989ed63ab9c0f4 +Subproject commit fc078ef6ad672d7f3e83008f0033db7480cc3ff0 diff --git a/Sources/Firelink/Settings/AboutSettingsPane.swift b/Sources/Firelink/Settings/AboutSettingsPane.swift index 2f5f1da..8b1ebb3 100644 --- a/Sources/Firelink/Settings/AboutSettingsPane.swift +++ b/Sources/Firelink/Settings/AboutSettingsPane.swift @@ -149,8 +149,6 @@ struct AboutSettingsPane: View { subtitle: "You have Firelink \(appVersion). Download the new release from GitHub when you're ready." ) - releaseNotesDisclosure(for: update) - HStack(spacing: 12) { Button { NSWorkspace.shared.open(update.releaseURL) @@ -243,22 +241,4 @@ struct AboutSettingsPane: View { } } - private func releaseNotesDisclosure(for update: AvailableReleaseUpdate) -> some View { - DisclosureGroup("What's New") { - ScrollView { - Text(releaseNotes(from: update.releaseNotes)) - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - .textSelection(.enabled) - } - .frame(maxHeight: 180) - .padding(8) - .background(Color(NSColor.controlBackgroundColor)) - .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) - } - } - - private func releaseNotes(from markdown: String) -> AttributedString { - (try? AttributedString(markdown: markdown)) ?? AttributedString(markdown) - } } diff --git a/Sources/Firelink/Settings/IntegrationSettingsPane.swift b/Sources/Firelink/Settings/IntegrationSettingsPane.swift index fab4600..cbefd30 100644 --- a/Sources/Firelink/Settings/IntegrationSettingsPane.swift +++ b/Sources/Firelink/Settings/IntegrationSettingsPane.swift @@ -7,85 +7,94 @@ struct IntegrationSettingsPane: View { @State private var showToast = false var body: some View { - ScrollView { - VStack(spacing: 24) { - // Header - HStack(alignment: .center, spacing: 16) { - Image(systemName: "puzzlepiece.extension.fill") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 48, height: 48) - .foregroundStyle(Color(nsColor: NSColor(red: 1.0, green: 0.44, blue: 0.22, alpha: 1.0))) - .accessibilityHidden(true) + VStack(spacing: 20) { + // Header + HStack(alignment: .center, spacing: 16) { + Image(systemName: "puzzlepiece.extension.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 40, height: 40) + .foregroundStyle(Color(nsColor: NSColor(red: 1.0, green: 0.44, blue: 0.22, alpha: 1.0))) + .accessibilityHidden(true) - VStack(alignment: .leading, spacing: 4) { - Text("Connect Browser Extension") - .font(.title.weight(.bold)) - Text("Capture downloads directly from your browser in three easy steps.") - .foregroundStyle(.secondary) - .font(.body) - } - Spacer() + VStack(alignment: .leading, spacing: 4) { + Text("Connect Browser Extension") + .font(.title2.weight(.bold)) + Text("Capture downloads directly from your browser in three easy steps.") + .foregroundStyle(.secondary) + .font(.subheadline) } - .padding(.bottom, 8) + Spacer() + } - KeychainAccessCard() + KeychainAccessCard() - if settings.isKeychainAccessGranted { - // Step 1: Copy Token - StepCardView( - stepNumber: 1, - title: "Copy Pairing Token", - description: "This secure token authorizes your browser extension.", - icon: "doc.on.clipboard.fill", - iconColor: .blue, - actionText: "Copy Token", - action: { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(settings.extensionPairingToken, forType: .string) - withAnimation { - showToast = true + if settings.isKeychainAccessGranted { + HStack(spacing: 12) { + // Step 1 + CompactStepCardView( + stepNumber: 1, + title: "Copy Token", + description: "This secure token authorizes your browser extension.", + icon: "doc.on.clipboard.fill", + iconColor: .blue, + actionText: "Copy Token", + action: { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(settings.extensionPairingToken, forType: .string) + withAnimation { + showToast = true + } + }, + secondaryActionText: "Regenerate", + secondaryAction: { + var bytes = [UInt8](repeating: 0, count: 32) + let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) + settings.extensionPairingToken = status == errSecSuccess ? Data(bytes).base64EncodedString() : UUID().uuidString } - }, - secondaryActionText: "Regenerate", - secondaryAction: { - var bytes = [UInt8](repeating: 0, count: 32) - let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) - settings.extensionPairingToken = status == errSecSuccess ? Data(bytes).base64EncodedString() : UUID().uuidString - } - ) + ) - // Step 2: Get Extension - StepCardView( - stepNumber: 2, - title: "Get Extension", - description: "Install the Firelink Companion extension on your favorite browser.", - icon: "globe", - iconColor: .orange, - actionText: "Firefox Add-ons", - action: { - if let url = URL(string: "https://addons.mozilla.org/en-US/firefox/addon/firelink-companion/") { - NSWorkspace.shared.open(url) - } - }, - secondaryActionText: "Releases", - secondaryAction: { - if let url = URL(string: "https://github.com/nimbold/Firelink-Extension/releases") { - NSWorkspace.shared.open(url) - } - } - ) + Image(systemName: "chevron.right") + .font(.title3.weight(.bold)) + .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) - // Step 3: Paste and Save - StepCardView( - stepNumber: 3, - title: "Paste & Connect", - description: "Click the Firelink icon in your browser's toolbar and paste the token into the App Pairing Token field.", - icon: "arrow.down.doc.fill", - iconColor: .green, - actionText: nil, - action: nil - ) + // Step 2 + CompactStepCardView( + stepNumber: 2, + title: "Get Extension", + description: "Install the Firelink Companion extension on your browser.", + icon: "globe", + iconColor: .orange, + actionText: "Firefox Add-ons", + action: { + if let url = URL(string: "https://addons.mozilla.org/en-US/firefox/addon/firelink-companion/") { + NSWorkspace.shared.open(url) + } + }, + secondaryActionText: "Releases", + secondaryAction: { + if let url = URL(string: "https://github.com/nimbold/Firelink-Extension/releases") { + NSWorkspace.shared.open(url) + } + } + ) + + Image(systemName: "chevron.right") + .font(.title3.weight(.bold)) + .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) + + // Step 3 + CompactStepCardView( + stepNumber: 3, + title: "Paste & Connect", + description: "Click the Firelink icon in your browser's toolbar and paste the token.", + icon: "arrow.down.doc.fill", + iconColor: .green, + actionText: nil, + action: nil + ) + } + .fixedSize(horizontal: false, vertical: true) Divider() @@ -103,18 +112,16 @@ struct IntegrationSettingsPane: View { } } .font(.footnote) - .padding(.top, 8) - - } } - .padding(32) + Spacer() } + .padding(24) .toast(isShowing: $showToast, message: "Token copied to clipboard!") .background(Color(NSColor.windowBackgroundColor)) } } -struct StepCardView: View { +struct CompactStepCardView: View { let stepNumber: Int let title: String let description: String @@ -126,83 +133,88 @@ struct StepCardView: View { var secondaryAction: (() -> Void)? = nil var body: some View { - HStack(spacing: 16) { - // Step Number Badge - ZStack { - Circle() - .fill(Color(nsColor: .controlBackgroundColor)) - .frame(width: 32, height: 32) - .shadow(color: .black.opacity(0.1), radius: 2, y: 1) - - Text("\(stepNumber)") - .font(.system(.headline, design: .rounded).weight(.bold)) - .foregroundStyle(.primary) - } - - // Icon - ZStack { - RoundedRectangle(cornerRadius: 12) - .fill(iconColor.opacity(0.15)) - .frame(width: 48, height: 48) - - Image(systemName: icon) - .font(.system(size: 24)) - .foregroundStyle(iconColor) + VStack(spacing: 12) { + HStack(alignment: .top) { + // Step Number Badge + ZStack { + Circle() + .fill(Color(nsColor: .controlBackgroundColor)) + .frame(width: 24, height: 24) + .shadow(color: .black.opacity(0.1), radius: 1, y: 1) + + Text("\(stepNumber)") + .font(.system(.caption, design: .rounded).weight(.bold)) + .foregroundStyle(.primary) + } + Spacer() + // Icon + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(iconColor.opacity(0.15)) + .frame(width: 32, height: 32) + + Image(systemName: icon) + .font(.system(size: 16)) + .foregroundStyle(iconColor) + } } // Text Content VStack(alignment: .leading, spacing: 4) { Text(title) - .font(.headline) + .font(.subheadline.weight(.semibold)) + .frame(maxWidth: .infinity, alignment: .leading) Text(description) - .font(.subheadline) + .font(.caption) .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) .fixedSize(horizontal: false, vertical: true) } - Spacer() + Spacer(minLength: 8) // Action Button - HStack(spacing: 8) { + VStack(spacing: 8) { + if let actionText = actionText, let action = action { + Button(action: action) { + Text(actionText) + .font(.caption.weight(.medium)) + .frame(maxWidth: .infinity) + .padding(.vertical, 6) + .background(Color.accentColor) + .foregroundColor(.white) + .cornerRadius(6) + } + .buttonStyle(.plain) + } + if let secondaryActionText = secondaryActionText, let secondaryAction = secondaryAction { Button(action: secondaryAction) { Text(secondaryActionText) - .font(.subheadline.weight(.medium)) - .padding(.horizontal, 16) - .padding(.vertical, 8) + .font(.caption.weight(.medium)) + .frame(maxWidth: .infinity) + .padding(.vertical, 6) .background(Color(nsColor: .controlBackgroundColor)) .foregroundColor(.primary) - .cornerRadius(8) + .cornerRadius(6) .overlay( - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: 6) .strokeBorder(Color(nsColor: .separatorColor).opacity(0.5), lineWidth: 1) ) } .buttonStyle(.plain) } - - if let actionText = actionText, let action = action { - Button(action: action) { - Text(actionText) - .font(.subheadline.weight(.medium)) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(Color.accentColor) - .foregroundColor(.white) - .cornerRadius(8) - } - .buttonStyle(.plain) - } } } - .padding(16) + .padding(12) + .frame(maxWidth: .infinity, maxHeight: .infinity) .background( - RoundedRectangle(cornerRadius: 16) + RoundedRectangle(cornerRadius: 12) .fill(Color(nsColor: .controlBackgroundColor)) - .shadow(color: .black.opacity(0.05), radius: 8, y: 2) + .shadow(color: .black.opacity(0.05), radius: 4, y: 1) ) .overlay( - RoundedRectangle(cornerRadius: 16) + RoundedRectangle(cornerRadius: 12) .strokeBorder(Color(nsColor: .separatorColor).opacity(0.5), lineWidth: 1) ) } diff --git a/Sources/Firelink/Settings/KeychainAccessCard.swift b/Sources/Firelink/Settings/KeychainAccessCard.swift index f198607..e51cad2 100644 --- a/Sources/Firelink/Settings/KeychainAccessCard.swift +++ b/Sources/Firelink/Settings/KeychainAccessCard.swift @@ -4,66 +4,61 @@ struct KeychainAccessCard: View { @EnvironmentObject private var settings: AppSettings var body: some View { - VStack(alignment: .leading, spacing: 16) { - HStack(spacing: 16) { - ZStack { - RoundedRectangle(cornerRadius: 12) - .fill(settings.isKeychainAccessGranted ? Color.green.opacity(0.15) : Color.blue.opacity(0.15)) - .frame(width: 48, height: 48) - - Image(systemName: settings.isKeychainAccessGranted ? "lock.open.fill" : "lock.fill") - .font(.system(size: 24)) - .foregroundStyle(settings.isKeychainAccessGranted ? .green : .blue) - } + HStack(spacing: 16) { + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(settings.isKeychainAccessGranted ? Color.green.opacity(0.15) : Color.blue.opacity(0.15)) + .frame(width: 36, height: 36) - VStack(alignment: .leading, spacing: 4) { - Text("Keychain Access") - .font(.headline) - Text("Firelink needs Keychain access to securely store your browser extension pairing token and site login passwords.") - .font(.subheadline) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - } - Spacer() + Image(systemName: settings.isKeychainAccessGranted ? "lock.open.fill" : "lock.fill") + .font(.system(size: 18)) + .foregroundStyle(settings.isKeychainAccessGranted ? .green : .blue) } - HStack { - Spacer() - if settings.isKeychainAccessGranted { - Label("Access Granted", systemImage: "checkmark.circle.fill") - .foregroundStyle(.green) - .font(.subheadline.weight(.medium)) - .padding(.trailing, 8) - - Button(role: .destructive) { - settings.revokeKeychainAccess() - } label: { - Text("Revoke Access") - } - } else { - Button { - settings.grantKeychainAccess() - } label: { - Text("Grant Access") - .font(.subheadline.weight(.medium)) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(Color.accentColor) - .foregroundColor(.white) - .cornerRadius(8) - } - .buttonStyle(.plain) + VStack(alignment: .leading, spacing: 2) { + Text("Keychain Access") + .font(.headline) + Text("Firelink needs Keychain access to securely store your browser extension pairing token.") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + Spacer(minLength: 16) + + if settings.isKeychainAccessGranted { + Label("Granted", systemImage: "checkmark.circle.fill") + .foregroundStyle(.green) + .font(.subheadline.weight(.medium)) + + Button(role: .destructive) { + settings.revokeKeychainAccess() + } label: { + Text("Revoke") } + } else { + Button { + settings.grantKeychainAccess() + } label: { + Text("Grant Access") + .font(.subheadline.weight(.medium)) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.accentColor) + .foregroundColor(.white) + .cornerRadius(6) + } + .buttonStyle(.plain) } } - .padding(16) + .padding(12) .background( - RoundedRectangle(cornerRadius: 16) + RoundedRectangle(cornerRadius: 12) .fill(Color(nsColor: .controlBackgroundColor)) - .shadow(color: .black.opacity(0.05), radius: 8, y: 2) + .shadow(color: .black.opacity(0.05), radius: 4, y: 1) ) .overlay( - RoundedRectangle(cornerRadius: 16) + RoundedRectangle(cornerRadius: 12) .strokeBorder(Color(nsColor: .separatorColor).opacity(0.5), lineWidth: 1) ) } diff --git a/Sources/Firelink/Settings/ToastNotification.swift b/Sources/Firelink/Settings/ToastNotification.swift index 5e1d7ca..e359f1d 100644 --- a/Sources/Firelink/Settings/ToastNotification.swift +++ b/Sources/Firelink/Settings/ToastNotification.swift @@ -26,11 +26,10 @@ struct ToastNotification: ViewModifier { } .zIndex(1) .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isShowing) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - withAnimation { - isShowing = false - } + .task { + try? await Task.sleep(nanoseconds: 2_000_000_000) + withAnimation { + isShowing = false } } }