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
This commit is contained in:
NimBold
2026-06-10 06:21:35 +03:30
parent 85f55db66f
commit 14924cf254
3 changed files with 228 additions and 97 deletions
@@ -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)
@@ -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: "")
}
}
}
}
}
@@ -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<SiteLogin.ID>()
@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 = ""
}
}
}