From 14924cf25468682eb57fc02729748f56e020cd3a Mon Sep 17 00:00:00 2001 From: NimBold Date: Wed, 10 Jun 2026 06:21:35 +0330 Subject: [PATCH] feat(settings): improve UI for Site Logins, Engine, and About panes - Refactor Site Logins pane to use a data Table with search and a dedicated editor sheet - Add 'Reveal in Finder' buttons for bundled engines in Engine pane - Fix alignment of 'Bundled' labels and 'Browser Cookies' picker in Engine pane - Remove build number from the version display in About pane --- .../Firelink/Settings/AboutSettingsPane.swift | 5 +- .../Settings/EngineSettingsPane.swift | 45 +-- .../Settings/SiteLoginsSettingsPane.swift | 275 +++++++++++++----- 3 files changed, 228 insertions(+), 97 deletions(-) diff --git a/Sources/Firelink/Settings/AboutSettingsPane.swift b/Sources/Firelink/Settings/AboutSettingsPane.swift index 918ddc2..282837f 100644 --- a/Sources/Firelink/Settings/AboutSettingsPane.swift +++ b/Sources/Firelink/Settings/AboutSettingsPane.swift @@ -15,9 +15,6 @@ struct AboutSettingsPane: View { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.1.0" } - private var buildNumber: String { - Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Development" - } var body: some View { Form { @@ -31,7 +28,7 @@ struct AboutSettingsPane: View { VStack(alignment: .leading, spacing: 4) { Text("Firelink") .font(.title2.weight(.bold)) - Text("Version \(appVersion) (\(buildNumber))") + Text("Version \(appVersion)") .foregroundStyle(.secondary) Text("A native macOS download manager for fast, organized, segmented transfers.") .font(.caption) diff --git a/Sources/Firelink/Settings/EngineSettingsPane.swift b/Sources/Firelink/Settings/EngineSettingsPane.swift index 326887f..abdf5b6 100644 --- a/Sources/Firelink/Settings/EngineSettingsPane.swift +++ b/Sources/Firelink/Settings/EngineSettingsPane.swift @@ -20,15 +20,16 @@ struct EngineSettingsPane: View { .textSelection(.enabled) } - LabeledContent("Binary Path") { - Text(executableURL?.path ?? "Not found") - .font(.system(.caption, design: .monospaced)) - .foregroundStyle(.tertiary) - .lineLimit(1) - .truncationMode(.middle) - .textSelection(.enabled) + LabeledContent("Binary") { + if let url = executableURL { + Button("Reveal in Finder") { + NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: "") + } + } else { + Text("Not found") + .foregroundStyle(.red) + } } - .help(executableURL?.path ?? "") } header: { Text("Core Downloader (Aria2)") } footer: { @@ -46,13 +47,16 @@ struct EngineSettingsPane: View { addonStatusRow(title: "FFmpeg", state: engineManager.ffmpegState, path: engineManager.binaryPath(for: .ffmpeg)) LabeledContent("Browser Cookies") { - Picker("", selection: $settings.mediaCookieSource) { - ForEach(BrowserCookieSource.allCases, id: \.self) { source in - Text(source.rawValue).tag(source) + HStack { + Spacer() + Picker("", selection: $settings.mediaCookieSource) { + ForEach(BrowserCookieSource.allCases, id: \.self) { source in + Text(source.rawValue).tag(source) + } } + .labelsHidden() + .fixedSize() } - .labelsHidden() - .frame(maxWidth: 200) } } header: { Text("Media Extractors") @@ -75,7 +79,7 @@ struct EngineSettingsPane: View { @ViewBuilder private func addonStatusRow(title: String, state: AddonState, path: URL?) -> some View { LabeledContent(title) { - VStack(alignment: .trailing) { + HStack(spacing: 8) { switch state { case .notInstalled: Text("Missing") @@ -89,13 +93,12 @@ struct EngineSettingsPane: View { .foregroundStyle(.red) .help(error) } - - Text(path?.path ?? "Not found") - .font(.system(.caption, design: .monospaced)) - .foregroundStyle(.tertiary) - .lineLimit(1) - .truncationMode(.middle) - .textSelection(.enabled) + + if let path { + Button("Reveal") { + NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: "") + } + } } } } diff --git a/Sources/Firelink/Settings/SiteLoginsSettingsPane.swift b/Sources/Firelink/Settings/SiteLoginsSettingsPane.swift index 97f13c8..e30b77a 100644 --- a/Sources/Firelink/Settings/SiteLoginsSettingsPane.swift +++ b/Sources/Firelink/Settings/SiteLoginsSettingsPane.swift @@ -1,11 +1,23 @@ import SwiftUI +import AppKit struct SiteLoginsSettingsPane: View { @EnvironmentObject private var settings: AppSettings - @State private var urlPattern = "" - @State private var username = "" - @State private var password = "" - @State private var editingLoginID: UUID? + @State private var searchText = "" + @State private var selection = Set() + @State private var editingLogin: SiteLogin? + @State private var showEditor = false + + var filteredLogins: [SiteLogin] { + if searchText.isEmpty { + return settings.siteLogins + } else { + return settings.siteLogins.filter { + $0.urlPattern.localizedCaseInsensitiveContains(searchText) || + $0.username.localizedCaseInsensitiveContains(searchText) + } + } + } var body: some View { Form { @@ -16,85 +28,204 @@ struct SiteLoginsSettingsPane: View { .listRowInsets(EdgeInsets()) if settings.isKeychainAccessGranted { - Section(editingLoginID == nil ? "Add Login" : "Edit Login") { - TextField("URL Pattern (e.g., *.github.com)", text: $urlPattern) - TextField("Username", text: $username) - SecureField(editingLoginID == nil ? "Password" : "Password (leave blank to keep current)", text: $password) - - HStack { - Text(settings.message) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) - Spacer() - if editingLoginID != nil { - Button("Cancel Edit") { - resetForm() - } - } - Button { - settings.saveSiteLogin( - id: editingLoginID, - urlPattern: urlPattern, - username: username, - password: password - ) - if settings.message.hasPrefix("Added") || settings.message.hasPrefix("Updated") { - resetForm() - } - } label: { - Label(editingLoginID == nil ? "Add Login" : "Save Login", systemImage: editingLoginID == nil ? "plus" : "checkmark") - } - .buttonStyle(.borderedProminent) - } - } - - Section("Saved Logins") { - if settings.siteLogins.isEmpty { - Text("No saved logins.") - .foregroundStyle(.secondary) - } else { - List { - ForEach(settings.siteLogins) { login in - HStack { - Image(systemName: "key.horizontal") - .foregroundStyle(.secondary) - .accessibilityHidden(true) - Text(login.urlPattern) - .font(.system(.body, design: .monospaced)) - Spacer() - Text(login.username) - .foregroundStyle(.secondary) + Section { + VStack(spacing: 0) { + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.secondary) + TextField("Search Logins", text: $searchText) + .textFieldStyle(.plain) + if !searchText.isEmpty { Button { - edit(login) + searchText = "" } label: { - Label("Edit", systemImage: "pencil") + Image(systemName: "xmark.circle.fill") + .foregroundColor(.secondary) } - .labelStyle(.iconOnly) - .buttonStyle(.borderless) + .buttonStyle(.plain) } } - .onDelete(perform: settings.deleteSiteLogins) + .padding(8) + .background(Color(NSColor.textBackgroundColor)) + + Divider() + + if settings.siteLogins.isEmpty { + ContentUnavailableView("No saved logins", systemImage: "key") + .frame(minHeight: 250) + } else if filteredLogins.isEmpty { + ContentUnavailableView.search(text: searchText) + .frame(minHeight: 250) + } else { + Table(filteredLogins, selection: $selection) { + TableColumn("URL Pattern") { login in + Text(login.urlPattern) + .font(.system(.body, design: .monospaced)) + .contextMenu { + contextMenuActions(for: login) + } + } + TableColumn("Username") { login in + Text(login.username) + .contextMenu { + contextMenuActions(for: login) + } + } + } + .frame(minHeight: 200, idealHeight: 250, maxHeight: 300) + } + + Divider() + + HStack { + Button { + editingLogin = nil + showEditor = true + } label: { + Image(systemName: "plus") + .frame(width: 24, height: 24) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .padding(.horizontal, 4) + + Divider() + .frame(height: 16) + + Button { + deleteSelected() + } label: { + Image(systemName: "minus") + .frame(width: 24, height: 24) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .padding(.horizontal, 4) + .disabled(selection.isEmpty) + + Spacer() + } + .padding(.vertical, 4) + .padding(.horizontal, 4) + .background(Color(NSColor.windowBackgroundColor)) } - .frame(minHeight: 180) - } + .background(Color(NSColor.controlBackgroundColor)) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.secondary.opacity(0.2), lineWidth: 1) + ) + } header: { + Text("Saved Logins") } + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) } } .formStyle(.grouped) + .sheet(isPresented: $showEditor) { + LoginEditorSheet(login: editingLogin) + } } - - private func edit(_ login: SiteLogin) { - editingLoginID = login.id - urlPattern = login.urlPattern - username = login.username - password = "" + + @ViewBuilder + private func contextMenuActions(for login: SiteLogin) -> some View { + Button("Edit") { + editingLogin = login + showEditor = true + } + Divider() + Button("Copy Username") { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(login.username, forType: .string) + } + Button("Copy Password") { + if let password = KeychainCredentialStore.password(for: login.id) { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(password, forType: .string) + } + } + Divider() + Button("Delete", role: .destructive) { + if let index = settings.siteLogins.firstIndex(where: { $0.id == login.id }) { + settings.deleteSiteLogins(at: IndexSet(integer: index)) + } + selection.remove(login.id) + } } - - private func resetForm() { - editingLoginID = nil - urlPattern = "" - username = "" - password = "" + + private func deleteSelected() { + let indices = settings.siteLogins.enumerated().compactMap { index, login in + selection.contains(login.id) ? index : nil + } + settings.deleteSiteLogins(at: IndexSet(indices)) + selection.removeAll() + } +} + +struct LoginEditorSheet: View { + @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var settings: AppSettings + + let login: SiteLogin? + + @State private var urlPattern = "" + @State private var username = "" + @State private var password = "" + + var body: some View { + VStack(spacing: 0) { + Text(login == nil ? "Add Login" : "Edit Login") + .font(.headline) + .padding() + + Form { + TextField("URL Pattern (e.g., *.github.com)", text: $urlPattern) + TextField("Username", text: $username) + SecureField(login == nil ? "Password" : "Password (leave blank to keep current)", text: $password) + } + .padding(.horizontal) + .padding(.bottom) + + if !settings.message.isEmpty && (settings.message.hasPrefix("Add a") || settings.message.hasPrefix("A login") || settings.message.hasPrefix("Could not")) { + Text(settings.message) + .foregroundColor(.red) + .font(.caption) + .padding(.bottom) + } + + HStack { + Button("Cancel") { + dismiss() + } + .keyboardShortcut(.cancelAction) + + Spacer() + + Button(login == nil ? "Add" : "Save") { + settings.message = "" + settings.saveSiteLogin( + id: login?.id, + urlPattern: urlPattern, + username: username, + password: password + ) + + if settings.message.hasPrefix("Added") || settings.message.hasPrefix("Updated") { + dismiss() + } + } + .keyboardShortcut(.defaultAction) + } + .padding() + } + .frame(width: 400) + .onAppear { + if let login = login { + urlPattern = login.urlPattern + username = login.username + } + settings.message = "" + } } }