mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
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:
@@ -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 = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user