From fa018dff0542cc82ac86b9ebba25b28c4111732e Mon Sep 17 00:00:00 2001 From: nimbold <11913706+nimbold@users.noreply.github.com> Date: Tue, 2 Jun 2026 04:05:36 +0330 Subject: [PATCH] Move settings into main sidebar --- README.md | 2 +- Sources/Firelink/ContentView.swift | 182 +++++++++++++++------------- Sources/Firelink/FirelinkApp.swift | 5 - Sources/Firelink/SettingsView.swift | 81 ++++++++++--- Sources/Firelink/SidebarView.swift | 43 +++++-- 5 files changed, 194 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index e3dd804..ebe601c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ## ✨ Key Features - ⚡ **High-Speed Downloads:** Multi-segmented download engine powered by `aria2c` for concurrent connections and optimal bandwidth utilization. Supports HTTP, HTTPS, FTP, and SFTP. -- 🎨 **Native macOS & SwiftUI:** Responsive interface designed natively for Apple Silicon, featuring resizable tables, customizable columns, sidebar filters, and a native Settings panel. +- 🎨 **Native macOS & SwiftUI:** Responsive interface designed natively for Apple Silicon, featuring resizable tables, customizable columns, sidebar filters, and an in-app Settings page. - 🗂️ **Smart Queue & Categories:** Drag-and-drop priority ordering, batch link ingestion with smart parsing, and automatic file organization (`Musics`, `Movies`, `Compressed`, `Pictures`, `Documents`, `Other`) based on extension detection. - 🔒 **Keychain Security:** Local macOS Keychain integration for secure site credential storage and matching during transfers. - ⚙️ **Power & System Integrity:** Optional system sleep prevention during active downloads, disk-space safety checks, and automated cleanup of partial `.aria2` metadata cache files. diff --git a/Sources/Firelink/ContentView.swift b/Sources/Firelink/ContentView.swift index a262df9..b859b23 100644 --- a/Sources/Firelink/ContentView.swift +++ b/Sources/Firelink/ContentView.swift @@ -5,111 +5,119 @@ struct ContentView: View { @EnvironmentObject private var controller: DownloadController @Environment(\.openWindow) private var openWindow @State private var selection: Set = [] - @State private var sidebarFilter: DownloadSidebarFilter = .all + @State private var sidebarSelection: SidebarSelection = .downloads(.all) @State private var showDeleteConfirmation = false var body: some View { NavigationSplitView { - SidebarView(selection: $sidebarFilter) + SidebarView(selection: $sidebarSelection) .navigationSplitViewColumnWidth(min: 190, ideal: 220, max: 260) } detail: { - VStack(spacing: 0) { - DownloadTable( - items: filteredDownloads, - selection: $selection, - title: sidebarFilter.title - ) - Divider() - StatusBar() + detailView + } + } + + @ViewBuilder + private var detailView: some View { + switch sidebarSelection { + case .downloads(let filter): + downloadsView(filter: filter) + case .settings: + SettingsView() + } + } + + private func downloadsView(filter: DownloadSidebarFilter) -> some View { + VStack(spacing: 0) { + DownloadTable( + items: filteredDownloads(for: filter), + selection: $selection, + title: filter.title + ) + Divider() + StatusBar() + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + openWindow(id: "add-downloads") + } label: { + Label("Add", systemImage: "plus") + } } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button { - openWindow(id: "add-downloads") - } label: { - Label("Add", systemImage: "plus") - } - } - - ToolbarItem { - Button { - controller.startQueue() - } label: { - Label("Start Queue", systemImage: "play.fill") - } + + ToolbarItem { + Button { + controller.startQueue() + } label: { + Label("Start Queue", systemImage: "play.fill") } + } - ToolbarItemGroup { - if !selectedItems.isEmpty { - if selectedItems.contains(where: { $0.status == .downloading }) { - Button { - for item in selectedItems where item.status == .downloading { - controller.pause(item) - } - } label: { - Label("Stop", systemImage: "stop.fill") + ToolbarItemGroup { + if !selectedItems.isEmpty { + if selectedItems.contains(where: { $0.status == .downloading }) { + Button { + for item in selectedItems where item.status == .downloading { + controller.pause(item) } - } - - if selectedItems.contains(where: { $0.status == .paused || $0.status == .failed || $0.status == .canceled }) { - Button { - for item in selectedItems where item.status == .paused || item.status == .failed || item.status == .canceled { - controller.resume(item) - } - } label: { - Label("Start", systemImage: "play.fill") - } - } - - Button(role: .destructive) { - showDeleteConfirmation = true } label: { - Label("Delete", systemImage: "trash") + Label("Stop", systemImage: "stop.fill") } } - } - ToolbarItem { - SettingsLink { - Label("Settings", systemImage: "gearshape") + if selectedItems.contains(where: { $0.status == .paused || $0.status == .failed || $0.status == .canceled }) { + Button { + for item in selectedItems where item.status == .paused || item.status == .failed || item.status == .canceled { + controller.resume(item) + } + } label: { + Label("Start", systemImage: "play.fill") + } } - } - } - .background { - Button("") { - if !selection.isEmpty { + + Button(role: .destructive) { showDeleteConfirmation = true + } label: { + Label("Delete", systemImage: "trash") } } - .keyboardShortcut(.delete, modifiers: []) - .opacity(0) - - Button("") { - handlePaste() + } + } + .background { + Button("") { + if !selection.isEmpty { + showDeleteConfirmation = true } - .keyboardShortcut("v", modifiers: .command) - .opacity(0) + } + .keyboardShortcut(.delete, modifiers: []) + .opacity(0) - Button("") { - selectAll() - } - .keyboardShortcut("a", modifiers: .command) - .opacity(0) + Button("") { + handlePaste() } - .confirmationDialog( - "Delete \(selection.count) Download\(selection.count == 1 ? "" : "s")", - isPresented: $showDeleteConfirmation - ) { - Button("Remove from List") { - deleteSelected(deleteFiles: false) - } - Button("Move Files and Cache to Trash", role: .destructive) { - deleteSelected(deleteFiles: true) - } - Button("Cancel", role: .cancel) {} - } message: { - Text("Are you sure you want to delete the selected downloads?") + .keyboardShortcut("v", modifiers: .command) + .opacity(0) + + Button("") { + selectAll(filter: filter) } + .keyboardShortcut("a", modifiers: .command) + .opacity(0) + } + .confirmationDialog( + "Delete \(selection.count) Download\(selection.count == 1 ? "" : "s")", + isPresented: $showDeleteConfirmation + ) { + Button("Remove from List") { + deleteSelected(deleteFiles: false) + } + Button("Move Files and Cache to Trash", role: .destructive) { + deleteSelected(deleteFiles: true) + } + Button("Cancel", role: .cancel) {} + } message: { + Text("Are you sure you want to delete the selected downloads?") } } @@ -130,12 +138,12 @@ struct ContentView: View { openWindow(id: "add-downloads") } - private func selectAll() { - selection = Set(filteredDownloads.map { $0.id }) + private func selectAll(filter: DownloadSidebarFilter) { + selection = Set(filteredDownloads(for: filter).map { $0.id }) } - private var filteredDownloads: [DownloadItem] { - switch sidebarFilter { + private func filteredDownloads(for filter: DownloadSidebarFilter) -> [DownloadItem] { + switch filter { case .all: controller.downloads case .queued: diff --git a/Sources/Firelink/FirelinkApp.swift b/Sources/Firelink/FirelinkApp.swift index b352640..90c7fed 100644 --- a/Sources/Firelink/FirelinkApp.swift +++ b/Sources/Firelink/FirelinkApp.swift @@ -46,10 +46,5 @@ struct FirelinkApp: App { .keyboardShortcut("r", modifiers: [.command]) } } - - Settings { - SettingsView() - .environmentObject(settings) - } } } diff --git a/Sources/Firelink/SettingsView.swift b/Sources/Firelink/SettingsView.swift index c1f4140..c6f989f 100644 --- a/Sources/Firelink/SettingsView.swift +++ b/Sources/Firelink/SettingsView.swift @@ -1,32 +1,75 @@ import SwiftUI +private enum SettingsSection: String, CaseIterable, Hashable { + case downloads = "Downloads" + case locations = "Locations" + case siteLogins = "Site Logins" + case power = "Power" + + var symbolName: String { + switch self { + case .downloads: "arrow.down.circle" + case .locations: "folder" + case .siteLogins: "key.fill" + case .power: "moon.zzz" + } + } +} + struct SettingsView: View { - @EnvironmentObject private var settings: AppSettings + @State private var selection: SettingsSection = .downloads var body: some View { - TabView { - DownloadSettingsPane() - .tabItem { - Label("Downloads", systemImage: "arrow.down.circle") - } + VStack(alignment: .leading, spacing: 0) { + Text("Settings") + .font(.largeTitle.weight(.semibold)) + .padding(.horizontal, 28) + .padding(.top, 26) + .padding(.bottom, 18) - LocationsSettingsPane() - .tabItem { - Label("Locations", systemImage: "folder") - } + Divider() - SiteLoginsSettingsPane() - .tabItem { - Label("Site Logins", systemImage: "key.fill") - } + HStack(spacing: 0) { + settingsSidebar - PowerSettingsPane() - .tabItem { - Label("Power", systemImage: "moon.zzz") + Divider() + + ScrollView { + selectedPane + .frame(maxWidth: 720, alignment: .leading) + .padding(28) + .frame(maxWidth: .infinity, alignment: .leading) } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + + private var settingsSidebar: some View { + List(selection: $selection) { + Section("Preferences") { + ForEach(SettingsSection.allCases, id: \.self) { section in + Label(section.rawValue, systemImage: section.symbolName) + .tag(section) + } + } + } + .listStyle(.sidebar) + .frame(width: 210) + } + + @ViewBuilder + private var selectedPane: some View { + switch selection { + case .downloads: + DownloadSettingsPane() + case .locations: + LocationsSettingsPane() + case .siteLogins: + SiteLoginsSettingsPane() + case .power: + PowerSettingsPane() } - .padding(20) - .frame(width: 680, height: 470) } } diff --git a/Sources/Firelink/SidebarView.swift b/Sources/Firelink/SidebarView.swift index 6a5a886..5a81279 100644 --- a/Sources/Firelink/SidebarView.swift +++ b/Sources/Firelink/SidebarView.swift @@ -20,35 +20,40 @@ enum DownloadSidebarFilter: Hashable { } } +enum SidebarSelection: Hashable { + case downloads(DownloadSidebarFilter) + case settings +} + struct SidebarView: View { @EnvironmentObject private var controller: DownloadController - @Binding var selection: DownloadSidebarFilter + @Binding var selection: SidebarSelection var body: some View { List(selection: $selection) { Section("Library") { Label("All", systemImage: "tray.full") .badge(controller.downloads.count) - .tag(DownloadSidebarFilter.all) + .tag(SidebarSelection.downloads(.all)) Label("Queue", systemImage: "list.bullet") .badge(controller.queuedCount) - .tag(DownloadSidebarFilter.queued) + .tag(SidebarSelection.downloads(.queued)) Label("Active", systemImage: "bolt.fill") .badge(controller.activeCount) - .tag(DownloadSidebarFilter.active) + .tag(SidebarSelection.downloads(.active)) Label("Completed", systemImage: "checkmark.circle") .badge(controller.completedCount) - .tag(DownloadSidebarFilter.completed) + .tag(SidebarSelection.downloads(.completed)) Label("Failed", systemImage: "exclamationmark.triangle") .badge(controller.failedCount) - .tag(DownloadSidebarFilter.failed) + .tag(SidebarSelection.downloads(.failed)) } Section("Folders") { ForEach(DownloadCategory.allCases, id: \.self) { category in Label(category.rawValue, systemImage: category.symbolName) .badge(controller.downloads.filter { $0.category == category }.count) - .tag(DownloadSidebarFilter.category(category)) + .tag(SidebarSelection.downloads(.category(category))) } } @@ -61,5 +66,29 @@ struct SidebarView: View { } } .listStyle(.sidebar) + .safeAreaInset(edge: .bottom) { + VStack(spacing: 8) { + Divider() + Button { + selection = .settings + } label: { + Label("Settings", systemImage: "gearshape") + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 10) + .padding(.vertical, 7) + .contentShape(RoundedRectangle(cornerRadius: 6)) + } + .buttonStyle(.plain) + .background { + if selection == .settings { + RoundedRectangle(cornerRadius: 6) + .fill(Color.accentColor.opacity(0.14)) + } + } + .padding(.horizontal, 8) + .padding(.bottom, 8) + } + .background(.bar) + } } }