From fe64e2553ae4986951e43b368027b05d93a588bb Mon Sep 17 00:00:00 2001 From: yonaries Date: Sat, 14 Mar 2026 17:59:16 +0300 Subject: [PATCH 1/5] refactor: downloads history as spatial sidebar destination Replace the downloads popover with a dedicated downloads history view that lives as a sibling destination in the sidebar. The transition uses a horizontal slide with push-back parallax on the spaces content. - Add DownloadsHistoryView with header, active/history sections, and empty state - Add DownloadHistoryRow with file icons, source hostname, and rich actions - Add interactive swipe gesture (right to dismiss, left from first space to enter) - Add retry, open file, and reveal in Finder actions - Wire floating sidebar visibility to downloads history state --- ora/Features/Browser/Views/BrowserView.swift | 4 +- .../Views/FloatingSidebarOverlay.swift | 4 +- .../Downloads/Services/DownloadManager.swift | 18 +- .../Downloads/Views/DownloadHistoryRow.swift | 259 ++++++++++++++++++ .../Views/DownloadsHistoryView.swift | 159 +++++++++++ .../Sidebar/Views/DownloadsWidget.swift | 32 +-- ora/Features/Sidebar/Views/SidebarView.swift | 101 ++++++- 7 files changed, 543 insertions(+), 34 deletions(-) create mode 100644 ora/Features/Downloads/Views/DownloadHistoryRow.swift create mode 100644 ora/Features/Downloads/Views/DownloadsHistoryView.swift diff --git a/ora/Features/Browser/Views/BrowserView.swift b/ora/Features/Browser/Views/BrowserView.swift index f044ae00..f155f9f5 100644 --- a/ora/Features/Browser/Views/BrowserView.swift +++ b/ora/Features/Browser/Views/BrowserView.swift @@ -85,7 +85,7 @@ struct BrowserView: View { showFloatingSidebar: $showFloatingSidebar, isMouseOverSidebar: $isMouseOverSidebar, sidebarFraction: sidebarManager.currentFraction, - isDownloadsPopoverOpen: downloadManager.isDownloadsPopoverOpen + isDownloadsOpen: downloadManager.isShowingDownloadsHistory ) } @@ -108,7 +108,7 @@ struct BrowserView: View { .onReceive(NotificationCenter.default.publisher(for: .toggleSidebarPosition)) { _ in sidebarManager.toggleSidebarPosition() } - .onChange(of: downloadManager.isDownloadsPopoverOpen) { _, isOpen in + .onChange(of: downloadManager.isShowingDownloadsHistory) { _, isOpen in if sidebarManager.isSidebarHidden { if isOpen { showFloatingSidebar = true diff --git a/ora/Features/Browser/Views/FloatingSidebarOverlay.swift b/ora/Features/Browser/Views/FloatingSidebarOverlay.swift index 77f50859..88bd05dc 100644 --- a/ora/Features/Browser/Views/FloatingSidebarOverlay.swift +++ b/ora/Features/Browser/Views/FloatingSidebarOverlay.swift @@ -10,7 +10,7 @@ struct FloatingSidebarOverlay: View { @Binding var isMouseOverSidebar: Bool var sidebarFraction: FractionHolder - let isDownloadsPopoverOpen: Bool + let isDownloadsOpen: Bool @State private var dragFraction: CGFloat? @@ -65,7 +65,7 @@ struct FloatingSidebarOverlay: View { get: { showFloatingSidebar }, set: { newValue in isMouseOverSidebar = newValue - if !newValue, isDownloadsPopoverOpen { + if !newValue, isDownloadsOpen { return } showFloatingSidebar = newValue diff --git a/ora/Features/Downloads/Services/DownloadManager.swift b/ora/Features/Downloads/Services/DownloadManager.swift index 9ac94bbe..185b6a4b 100644 --- a/ora/Features/Downloads/Services/DownloadManager.swift +++ b/ora/Features/Downloads/Services/DownloadManager.swift @@ -6,7 +6,7 @@ import SwiftUI class DownloadManager: ObservableObject { @Published var activeDownloads: [Download] = [] @Published var recentDownloads: [Download] = [] - @Published var isDownloadsPopoverOpen = false + @Published var isShowingDownloadsHistory = false let modelContainer: ModelContainer let modelContext: ModelContext @@ -32,7 +32,7 @@ class DownloadManager: ObservableObject { do { let downloads = try modelContext.fetch(descriptor) - self.recentDownloads = Array(downloads.prefix(20)) // Show last 20 downloads + self.recentDownloads = Array(downloads.prefix(50)) self.activeDownloads = downloads.filter { $0.status == .downloading } } catch { // Failed to load downloads @@ -201,6 +201,20 @@ class DownloadManager: ObservableObject { NSWorkspace.shared.selectFile(destinationURL.path, inFileViewerRootedAtPath: "") } + func openFile(_ download: Download) { + guard let url = download.destinationURL else { return } + NSWorkspace.shared.open(url) + } + + /// Re-opens the original URL in the browser to re-trigger the download + func retryDownload(_ download: Download) { + guard let url = URL(string: download.originalURLString) else { return } + deleteDownload(download) + if let window = NSApp.keyWindow { + NotificationCenter.default.post(name: .openURL, object: window, userInfo: ["url": url]) + } + } + private func refreshRecentDownloads() { loadRecentDownloads() } diff --git a/ora/Features/Downloads/Views/DownloadHistoryRow.swift b/ora/Features/Downloads/Views/DownloadHistoryRow.swift new file mode 100644 index 00000000..8708e747 --- /dev/null +++ b/ora/Features/Downloads/Views/DownloadHistoryRow.swift @@ -0,0 +1,259 @@ +import SwiftUI + +struct DownloadHistoryRow: View { + let download: Download + @EnvironmentObject var downloadManager: DownloadManager + @Environment(\.theme) private var theme + @State private var isHovered = false + + var body: some View { + HStack(spacing: 10) { + fileIconView + + VStack(alignment: .leading, spacing: 2) { + Text(download.fileName) + .font(.system(size: 12, weight: .medium)) + .foregroundColor(theme.foreground) + .lineLimit(1) + .truncationMode(.middle) + + HStack(spacing: 4) { + if let hostname = sourceHostname { + Text(hostname) + .font(.system(size: 10)) + .foregroundColor(.secondary) + .lineLimit(1) + + if !statusText.isEmpty { + Text("\u{00B7}") + .font(.system(size: 10)) + .foregroundColor(.secondary) + } + } + + Text(statusText) + .font(.system(size: 10)) + .foregroundColor(statusColor) + .lineLimit(1) + + Spacer() + + if download.status == .completed { + Text(download.formattedFileSize) + .font(.system(size: 10)) + .foregroundColor(.secondary) + } + } + + if download.status == .downloading { + progressBar + } + } + + if isHovered || download.status == .downloading { + actionButtons + } + } + .padding(.horizontal, 6) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(isHovered ? theme.mutedBackground.opacity(0.5) : .clear) + ) + .contentShape(Rectangle()) + .onHover { hovering in + withAnimation(.easeOut(duration: 0.15)) { + isHovered = hovering + } + } + .onTapGesture { + if download.status == .completed { + downloadManager.openFile(download) + } + } + .contextMenu { + DownloadHistoryContextMenu(download: download) + } + } + + // MARK: - Subviews + + private var fileIconView: some View { + ZStack { + RoundedRectangle(cornerRadius: 6) + .fill(iconColor.opacity(0.12)) + .frame(width: 32, height: 32) + + Image(systemName: fileIcon) + .font(.system(size: 14)) + .foregroundColor(iconColor) + } + } + + private var progressBar: some View { + GeometryReader { geo in + ZStack(alignment: .leading) { + Capsule() + .fill(theme.mutedBackground) + .frame(height: 3) + + Capsule() + .fill(Color.blue) + .frame(width: geo.size.width * download.displayProgress, height: 3) + .animation(.easeOut(duration: 0.2), value: download.displayProgress) + } + } + .frame(height: 3) + .padding(.top, 2) + } + + private var actionButtons: some View { + HStack(spacing: 2) { + switch download.status { + case .downloading: + iconButton("xmark.circle.fill", color: .secondary) { + downloadManager.cancelDownload(download) + } + case .completed: + iconButton("folder", color: .secondary) { + downloadManager.openDownloadInFinder(download) + } + case .failed, .cancelled: + iconButton("arrow.clockwise", color: .blue) { + downloadManager.retryDownload(download) + } + default: + EmptyView() + } + + if download.status != .downloading { + iconButton("xmark", color: .secondary) { + withAnimation(.easeOut(duration: 0.2)) { + downloadManager.deleteDownload(download) + } + } + } + } + } + + private func iconButton(_ systemName: String, color: Color, action: @escaping () -> Void) -> some View { + Button(action: action) { + Image(systemName: systemName) + .font(.system(size: 11)) + .foregroundColor(color) + .frame(width: 20, height: 20) + } + .buttonStyle(.plain) + } + + // MARK: - Computed Properties + + private var sourceHostname: String? { + guard let url = URL(string: download.originalURLString) else { return nil } + return url.host?.replacingOccurrences(of: "www.", with: "") + } + + private var fileIcon: String { + let ext = (download.fileName as NSString).pathExtension.lowercased() + switch ext { + case "pdf": return "doc.fill" + case "zip", "rar", "7z", "tar", "gz": return "archivebox.fill" + case "jpg", "jpeg", "png", "gif", "webp", "svg": return "photo.fill" + case "mp4", "mov", "avi", "mkv", "webm": return "video.fill" + case "mp3", "wav", "flac", "aac", "ogg": return "music.note" + case "dmg", "pkg", "app": return "app.fill" + case "html", "htm": return "globe" + case "txt", "md", "rtf": return "doc.text.fill" + case "json", "xml", "csv": return "tablecells.fill" + case "swift", "js", "py", "rb", "go": return "chevron.left.forwardslash.chevron.right" + default: return "doc.fill" + } + } + + private var iconColor: Color { + switch download.status { + case .downloading: return .blue + case .completed: return .green + case .failed: return .red + case .cancelled: return .orange + default: return .gray + } + } + + private var statusColor: Color { + switch download.status { + case .downloading: return .blue + case .failed: return .red + case .cancelled: return .orange + default: return .secondary + } + } + + private var statusText: String { + switch download.status { + case .downloading: + if download.displayFileSize > 0 { + return "\(download.formattedDownloadedSize) of \(download.formattedFileSize) \u{00B7} \(Int(download.displayProgress * 100))%" + } + return download.formattedDownloadedSize + case .completed: + return timeAgo(from: download.completedAt ?? download.createdAt) + case .failed: + return "Failed" + case .cancelled: + return "Cancelled" + default: + return "Pending" + } + } + + private func timeAgo(from date: Date) -> String { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .abbreviated + return formatter.localizedString(for: date, relativeTo: Date()) + } +} + +struct DownloadHistoryContextMenu: View { + let download: Download + @EnvironmentObject var downloadManager: DownloadManager + + var body: some View { + Group { + if download.status == .completed { + Button("Open") { + downloadManager.openFile(download) + } + + Button("Show in Finder") { + downloadManager.openDownloadInFinder(download) + } + + Button("Copy Path") { + if let path = download.destinationURL?.path { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(path, forType: .string) + } + } + } + + if download.status == .downloading { + Button("Cancel Download") { + downloadManager.cancelDownload(download) + } + } + + if download.status == .failed || download.status == .cancelled { + Button("Retry Download") { + downloadManager.retryDownload(download) + } + } + + Divider() + + Button("Remove from History") { + downloadManager.deleteDownload(download) + } + } + } +} diff --git a/ora/Features/Downloads/Views/DownloadsHistoryView.swift b/ora/Features/Downloads/Views/DownloadsHistoryView.swift new file mode 100644 index 00000000..e5b0659f --- /dev/null +++ b/ora/Features/Downloads/Views/DownloadsHistoryView.swift @@ -0,0 +1,159 @@ +import SwiftUI + +struct DownloadsHistoryView: View { + @EnvironmentObject var downloadManager: DownloadManager + @EnvironmentObject var sidebarManager: SidebarManager + @EnvironmentObject var appState: AppState + @Environment(\.theme) private var theme + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + header + Divider().opacity(0.5) + content + Spacer(minLength: 0) + footer + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(theme.subtleWindowBackgroundColor) + .background(BlurEffectView(material: .underWindowBackground, blendingMode: .behindWindow)) + } + + // MARK: - Header + + private var header: some View { + HStack(spacing: 0) { + // Match SidebarHeader traffic light spacing when sidebar is primary + if sidebarManager.sidebarPosition != .secondary { + WindowControls(isFullscreen: appState.isFullscreen) + .frame(height: 30) + } + + Text("Downloads") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(theme.foreground) + .lineLimit(1) + + Spacer() + + if hasNonActiveDownloads { + Button(action: { + downloadManager.clearCompletedDownloads() + }) { + Text("Clear") + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 10) + .frame(height: 38) + } + + // MARK: - Footer + + private var footer: some View { + HStack { + Button(action: dismissDownloads) { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + .font(.system(size: 11, weight: .semibold)) + Text("Spaces") + .font(.system(size: 12, weight: .medium)) + } + .foregroundColor(theme.foreground.opacity(0.7)) + } + .buttonStyle(.plain) + + Spacer() + } + .padding(.horizontal, 10) + .padding(.bottom, 10) + } + + // MARK: - Content + + @ViewBuilder + private var content: some View { + if downloadManager.activeDownloads.isEmpty, downloadManager.recentDownloads.isEmpty { + emptyState + } else { + ScrollView(.vertical, showsIndicators: false) { + LazyVStack(alignment: .leading, spacing: 0) { + // Active downloads section + if !downloadManager.activeDownloads.isEmpty { + sectionHeader("Active") + ForEach(downloadManager.activeDownloads) { download in + DownloadHistoryRow(download: download) + } + } + + // History section (non-active downloads) + let historyDownloads = downloadManager.recentDownloads.filter { + $0.status != .downloading + } + if !historyDownloads.isEmpty { + if !downloadManager.activeDownloads.isEmpty { + Divider().opacity(0.3).padding(.vertical, 4) + } + sectionHeader("History") + ForEach(historyDownloads) { download in + DownloadHistoryRow(download: download) + } + } + } + .padding(.horizontal, 10) + } + } + } + + // MARK: - Empty State + + private var emptyState: some View { + VStack(spacing: 12) { + Spacer() + + Image(systemName: "arrow.down.circle") + .font(.system(size: 36, weight: .light)) + .foregroundColor(theme.mutedForeground) + + Text("No Downloads") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(theme.foreground) + + Text("Files you download will appear here") + .font(.system(size: 12)) + .foregroundColor(theme.mutedForeground) + .multilineTextAlignment(.center) + + Spacer() + } + .frame(maxWidth: .infinity) + .padding(24) + } + + // MARK: - Helpers + + private func sectionHeader(_ title: String) -> some View { + Text(title) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.secondary) + .textCase(.uppercase) + .padding(.horizontal, 6) + .padding(.top, 10) + .padding(.bottom, 4) + } + + private var hasNonActiveDownloads: Bool { + downloadManager.recentDownloads.contains { + $0.status == .completed || $0.status == .failed || $0.status == .cancelled + } + } + + private func dismissDownloads() { + withAnimation(.spring(response: 0.45, dampingFraction: 0.88)) { + downloadManager.isShowingDownloadsHistory = false + } + } +} diff --git a/ora/Features/Sidebar/Views/DownloadsWidget.swift b/ora/Features/Sidebar/Views/DownloadsWidget.swift index 7b56ed1e..0d5a44d1 100644 --- a/ora/Features/Sidebar/Views/DownloadsWidget.swift +++ b/ora/Features/Sidebar/Views/DownloadsWidget.swift @@ -7,7 +7,7 @@ struct DownloadsWidget: View { var body: some View { VStack(spacing: 0) { - // Active downloads + // Active downloads (compact inline progress) if !downloadManager.activeDownloads.isEmpty { VStack(spacing: 6) { ForEach(downloadManager.activeDownloads) { download in @@ -19,34 +19,16 @@ struct DownloadsWidget: View { .padding(.bottom, 8) } - // Downloads button + // Downloads button - opens the downloads history destination Button(action: { - downloadManager.isDownloadsPopoverOpen.toggle() + withAnimation(.spring(response: 0.45, dampingFraction: 0.88)) { + downloadManager.isShowingDownloadsHistory.toggle() + } }) { HStack(spacing: 8) { Image(systemName: "arrow.down") .foregroundColor(downloadButtonColor) .frame(width: 12, height: 12) - - // Text("Downloads") - // .font(.system(size: 13, weight: .medium)) - // .foregroundColor(theme.foreground) - - // Spacer() - - // if !downloadManager.recentDownloads.isEmpty { - // Text("\(downloadManager.recentDownloads.count)") - // .font(.system(size: 11, weight: .medium)) - // .foregroundColor(.secondary) - // .padding(.horizontal, 6) - // .padding(.vertical, 2) - // .background(theme.background.opacity(0.6)) - // .cornerRadius(8) - // } - - // Image(systemName: downloadManager.isDownloadsPopoverOpen ? "chevron.up" : "chevron.down") - // .foregroundColor(.secondary) - // .frame(width: 12, height: 12) } .padding(8) .background(isHovered ? theme.invertedSolidWindowBackgroundColor.opacity(0.3) : .clear) @@ -58,10 +40,6 @@ struct DownloadsWidget: View { isHovered = hovering } } - .popover(isPresented: $downloadManager.isDownloadsPopoverOpen, arrowEdge: .bottom) { - DownloadsListView() - .environmentObject(downloadManager) - } } } diff --git a/ora/Features/Sidebar/Views/SidebarView.swift b/ora/Features/Sidebar/Views/SidebarView.swift index 91d8758d..bd4139cb 100644 --- a/ora/Features/Sidebar/Views/SidebarView.swift +++ b/ora/Features/Sidebar/Views/SidebarView.swift @@ -25,6 +25,13 @@ struct SidebarView: View { @State private var isHoveringSidebarToggle = false + /// Downloads transition state + @State private var dragOffset: CGFloat = 0 + + private var isShowingDownloads: Bool { + downloadManager.isShowingDownloadsHistory + } + private var shouldShowMediaWidget: Bool { let activeId = tabManager.activeTab?.id let others = media.visibleSessions.filter { session in @@ -50,6 +57,99 @@ struct SidebarView: View { } var body: some View { + GeometryReader { geo in + let width = geo.size.width + let progress = transitionProgress(for: width) + + ZStack(alignment: .leading) { + // Spaces content - pushes back when downloads is shown + spacesContent + .frame(width: width) + .offset(x: width * 0.12 * progress) + .scaleEffect(CGFloat(1.0) - 0.06 * progress, anchor: .center) + .opacity(CGFloat(1.0) - 0.5 * progress) + .allowsHitTesting(progress < 0.5) + + // Downloads history - slides in from leading edge + DownloadsHistoryView() + .frame(width: width) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) + .offset(x: -width + width * progress) + .shadow(color: .black.opacity(0.08 * Double(progress)), radius: 8, x: 4, y: 0) + .allowsHitTesting(progress >= 0.5) + } + .clipped() + // Swipe-to-dismiss gesture on the whole sidebar when downloads is showing + .simultaneousGesture(downloadsNavigationGesture(width: width)) + } + .enableInjection() + } + + /// Computes transition progress (0 = spaces visible, 1 = downloads visible) + /// incorporating both the boolean state and any interactive drag offset. + private func transitionProgress(for width: CGFloat) -> CGFloat { + let base: CGFloat = isShowingDownloads ? 1.0 : 0.0 + // dragOffset > 0 means dragging right (toward spaces), < 0 means dragging left (toward downloads) + let dragContribution = -dragOffset / max(width, 1) + return min(1, max(0, base + dragContribution)) + } + + // MARK: - Gesture + + /// Handles swipe-to-dismiss (right swipe when in downloads) and + /// swipe-to-enter (left swipe from first container). + private func downloadsNavigationGesture(width: CGFloat) -> some Gesture { + DragGesture(minimumDistance: 30) + .onChanged { value in + if isShowingDownloads { + // Swipe right to dismiss downloads + if value.translation.width > 0 { + dragOffset = value.translation.width + } + } else if selectedContainerIndex.wrappedValue == 0 { + // Swipe left from first container to show downloads + if value.translation.width < 0 { + dragOffset = value.translation.width + } + } + } + .onEnded { value in + let threshold = width * 0.25 + if isShowingDownloads { + if value.translation.width > threshold + || value.predictedEndTranslation.width > threshold * 2 + { + withAnimation(.spring(response: 0.45, dampingFraction: 0.88)) { + downloadManager.isShowingDownloadsHistory = false + dragOffset = 0 + } + } else { + withAnimation(.spring(response: 0.35, dampingFraction: 0.9)) { + dragOffset = 0 + } + } + } else if selectedContainerIndex.wrappedValue == 0 { + if -value.translation.width > threshold + || -value.predictedEndTranslation.width > threshold * 2 + { + withAnimation(.spring(response: 0.45, dampingFraction: 0.88)) { + downloadManager.isShowingDownloadsHistory = true + dragOffset = 0 + } + } else { + withAnimation(.spring(response: 0.35, dampingFraction: 0.9)) { + dragOffset = 0 + } + } + } else { + dragOffset = 0 + } + } + } + + // MARK: - Spaces Content + + private var spacesContent: some View { VStack(alignment: .leading, spacing: 0) { if sidebarManager.sidebarPosition == .secondary, !toolbarManager.isToolbarHidden { Spacer().frame(height: 8) @@ -105,7 +205,6 @@ struct SidebarView: View { .onTapGesture(count: 2) { toggleMaximizeWindow() } - .enableInjection() } private func onContainerSelected(container: TabContainer) { From e207ee2422276a111b2b4eef95d9c253681424ad Mon Sep 17 00:00:00 2001 From: yonaries Date: Sat, 14 Mar 2026 18:10:08 +0300 Subject: [PATCH 2/5] feat: add search and date-grouped history to downloads view Add OraInput search bar to filter downloads by file name or URL. Group history downloads by date buckets (Today, Yesterday, This Week, Last Week, etc.) for easier navigation. --- .../Views/DownloadsHistoryView.swift | 126 ++++++++++++++++-- 1 file changed, 115 insertions(+), 11 deletions(-) diff --git a/ora/Features/Downloads/Views/DownloadsHistoryView.swift b/ora/Features/Downloads/Views/DownloadsHistoryView.swift index e5b0659f..8555ee69 100644 --- a/ora/Features/Downloads/Views/DownloadsHistoryView.swift +++ b/ora/Features/Downloads/Views/DownloadsHistoryView.swift @@ -6,9 +6,16 @@ struct DownloadsHistoryView: View { @EnvironmentObject var appState: AppState @Environment(\.theme) private var theme + @State private var searchText = "" + + private var isSearching: Bool { + !searchText.isEmpty + } + var body: some View { VStack(alignment: .leading, spacing: 0) { header + searchBar Divider().opacity(0.5) content Spacer(minLength: 0) @@ -51,6 +58,20 @@ struct DownloadsHistoryView: View { .frame(height: 38) } + // MARK: - Search Bar + + private var searchBar: some View { + OraInput( + text: $searchText, + placeholder: "Search files...", + variant: .ghost, + size: .sm, + leadingIcon: "magnifyingglass" + ) + .padding(.horizontal, 10) + .padding(.vertical, 4) + } + // MARK: - Footer private var footer: some View { @@ -76,29 +97,32 @@ struct DownloadsHistoryView: View { @ViewBuilder private var content: some View { + let filteredActive = filteredDownloads(from: downloadManager.activeDownloads) + let filteredHistory = filteredDownloads( + from: downloadManager.recentDownloads.filter { $0.status != .downloading } + ) + let groupedHistory = groupByDate(filteredHistory) + if downloadManager.activeDownloads.isEmpty, downloadManager.recentDownloads.isEmpty { emptyState + } else if isSearching, filteredActive.isEmpty, filteredHistory.isEmpty { + noResultsState } else { ScrollView(.vertical, showsIndicators: false) { LazyVStack(alignment: .leading, spacing: 0) { - // Active downloads section - if !downloadManager.activeDownloads.isEmpty { + if !filteredActive.isEmpty { sectionHeader("Active") - ForEach(downloadManager.activeDownloads) { download in + ForEach(filteredActive) { download in DownloadHistoryRow(download: download) } } - // History section (non-active downloads) - let historyDownloads = downloadManager.recentDownloads.filter { - $0.status != .downloading - } - if !historyDownloads.isEmpty { - if !downloadManager.activeDownloads.isEmpty { + ForEach(groupedHistory, id: \.label) { group in + if !filteredActive.isEmpty || group.label != groupedHistory.first?.label { Divider().opacity(0.3).padding(.vertical, 4) } - sectionHeader("History") - ForEach(historyDownloads) { download in + sectionHeader(group.label) + ForEach(group.downloads) { download in DownloadHistoryRow(download: download) } } @@ -133,8 +157,88 @@ struct DownloadsHistoryView: View { .padding(24) } + // MARK: - No Results State + + private var noResultsState: some View { + VStack(spacing: 8) { + Spacer() + Text("No results for \u{201C}\(searchText)\u{201D}") + .font(.system(size: 13)) + .foregroundColor(theme.mutedForeground) + .lineLimit(2) + .multilineTextAlignment(.center) + Spacer() + } + .frame(maxWidth: .infinity) + .padding(24) + } + // MARK: - Helpers + private struct DateGroup { + let label: String + let downloads: [Download] + } + + private func filteredDownloads(from downloads: [Download]) -> [Download] { + guard isSearching else { return downloads } + let query = searchText.lowercased() + return downloads.filter { download in + download.fileName.lowercased().contains(query) + || download.originalURLString.lowercased().contains(query) + } + } + + private func groupByDate(_ downloads: [Download]) -> [DateGroup] { + guard !downloads.isEmpty else { return [] } + + let calendar = Calendar.current + let now = Date() + + let startOfToday = calendar.startOfDay(for: now) + let startOfYesterday = calendar.date(byAdding: .day, value: -1, to: startOfToday)! + let startOfThisWeek = calendar.date(from: calendar.dateComponents( + [.yearForWeekOfYear, .weekOfYear], + from: now + ))! + let startOfLastWeek = calendar.date(byAdding: .weekOfYear, value: -1, to: startOfThisWeek)! + let startOf2WeeksAgo = calendar.date(byAdding: .weekOfYear, value: -2, to: startOfThisWeek)! + let startOf1MonthAgo = calendar.date(byAdding: .month, value: -1, to: startOfToday)! + let startOf2MonthsAgo = calendar.date(byAdding: .month, value: -2, to: startOfToday)! + let startOf3MonthsAgo = calendar.date(byAdding: .month, value: -3, to: startOfToday)! + let startOf6MonthsAgo = calendar.date(byAdding: .month, value: -6, to: startOfToday)! + let startOf1YearAgo = calendar.date(byAdding: .year, value: -1, to: startOfToday)! + + // Ordered buckets: (label, lowerBound). A download goes into the first bucket + // whose lowerBound is <= its date. + let buckets: [(String, Date)] = [ + ("Today", startOfToday), + ("Yesterday", startOfYesterday), + ("This Week", startOfThisWeek), + ("Last Week", startOfLastWeek), + ("2 Weeks Ago", startOf2WeeksAgo), + ("Last Month", startOf1MonthAgo), + ("2 Months Ago", startOf2MonthsAgo), + ("3 Months Ago", startOf3MonthsAgo), + ("6 Months Ago", startOf6MonthsAgo), + ("Last Year", startOf1YearAgo), + ("Older", .distantPast) + ] + + var grouped: [String: [Download]] = [:] + for download in downloads { + let date = download.completedAt ?? download.createdAt + let label = buckets.first { date >= $0.1 }?.0 ?? "Older" + grouped[label, default: []].append(download) + } + + // Return in bucket order, skipping empty groups + return buckets.compactMap { label, _ in + guard let items = grouped[label], !items.isEmpty else { return nil } + return DateGroup(label: label, downloads: items) + } + } + private func sectionHeader(_ title: String) -> some View { Text(title) .font(.system(size: 11, weight: .medium)) From 38a2b8eae067cb9528a781ee666ed7b49b433274 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sat, 14 Mar 2026 19:29:44 +0300 Subject: [PATCH 3/5] feat: polish downloads UI and auto-close download-only tabs - Use native macOS file icons (NSWorkspace) instead of SF Symbols - Replace inline download progress with circular ring on sidebar icon - Use accent color for progress bar, retry icon, and status text - Use OraIcons (DownloadBox, Brush1) for sidebar and clear button - Auto-close tabs opened solely for file downloads (no HTML loaded) - Navigate back when a page triggers a download from existing content - Fix active downloads not immediately appearing in the widget --- ora/Core/BrowserEngine/BrowserPage.swift | 4 +- .../Downloads/Services/DownloadManager.swift | 5 + .../Downloads/Views/DownloadHistoryRow.swift | 62 ++++----- .../Views/DownloadsHistoryView.swift | 11 +- .../Sidebar/Views/DownloadsWidget.swift | 79 ++++++------ ora/Features/Sidebar/Views/SidebarView.swift | 2 +- .../Tabs/Browser/TabBrowserPageDelegate.swift | 8 ++ ora/Shared/Components/Icons/Brush1.swift | 80 ++++++++++++ ora/Shared/Components/Icons/Brush2.swift | 117 +++++++++++++++++ ora/Shared/Components/Icons/DownloadBox.swift | 80 ++++++++++++ .../Components/Icons/DownloadBox2.swift | 119 ++++++++++++++++++ ora/Shared/Components/Icons/OraIcon.swift | 10 +- 12 files changed, 496 insertions(+), 81 deletions(-) create mode 100644 ora/Shared/Components/Icons/Brush1.swift create mode 100644 ora/Shared/Components/Icons/Brush2.swift create mode 100644 ora/Shared/Components/Icons/DownloadBox.swift create mode 100644 ora/Shared/Components/Icons/DownloadBox2.swift diff --git a/ora/Core/BrowserEngine/BrowserPage.swift b/ora/Core/BrowserEngine/BrowserPage.swift index c262f8dc..0f201338 100644 --- a/ora/Core/BrowserEngine/BrowserPage.swift +++ b/ora/Core/BrowserEngine/BrowserPage.swift @@ -8,8 +8,8 @@ final class BrowserPage: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptM private let webView: WKWebView private let messageNames: [String] private var originalURL: URL? - private var lastCommittedURL: URL? - private var isDownloadNavigation = false + private(set) var lastCommittedURL: URL? + private(set) var isDownloadNavigation = false init( profile: BrowserEngineProfile, diff --git a/ora/Features/Downloads/Services/DownloadManager.swift b/ora/Features/Downloads/Services/DownloadManager.swift index 185b6a4b..c794a482 100644 --- a/ora/Features/Downloads/Services/DownloadManager.swift +++ b/ora/Features/Downloads/Services/DownloadManager.swift @@ -66,6 +66,11 @@ class DownloadManager: ObservableObject { activeDownloads.append(download) refreshRecentDownloads() + // Ensure SwiftUI picks up the change when called from WKDownload callbacks + DispatchQueue.main.async { + self.objectWillChange.send() + } + toastManager?.show("Downloading \(suggestedFilename)", type: .info, icon: .system("arrow.down.circle")) return download diff --git a/ora/Features/Downloads/Views/DownloadHistoryRow.swift b/ora/Features/Downloads/Views/DownloadHistoryRow.swift index 8708e747..f7df6dca 100644 --- a/ora/Features/Downloads/Views/DownloadHistoryRow.swift +++ b/ora/Features/Downloads/Views/DownloadHistoryRow.swift @@ -1,4 +1,5 @@ import SwiftUI +import UniformTypeIdentifiers struct DownloadHistoryRow: View { let download: Download @@ -23,11 +24,13 @@ struct DownloadHistoryRow: View { .font(.system(size: 10)) .foregroundColor(.secondary) .lineLimit(1) + .truncationMode(.tail) if !statusText.isEmpty { Text("\u{00B7}") .font(.system(size: 10)) .foregroundColor(.secondary) + .layoutPriority(1) } } @@ -35,13 +38,16 @@ struct DownloadHistoryRow: View { .font(.system(size: 10)) .foregroundColor(statusColor) .lineLimit(1) + .truncationMode(.tail) - Spacer() + Spacer(minLength: 0) if download.status == .completed { Text(download.formattedFileSize) .font(.system(size: 10)) .foregroundColor(.secondary) + .lineLimit(1) + .fixedSize() } } @@ -79,15 +85,10 @@ struct DownloadHistoryRow: View { // MARK: - Subviews private var fileIconView: some View { - ZStack { - RoundedRectangle(cornerRadius: 6) - .fill(iconColor.opacity(0.12)) - .frame(width: 32, height: 32) - - Image(systemName: fileIcon) - .font(.system(size: 14)) - .foregroundColor(iconColor) - } + Image(nsImage: nativeFileIcon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 32, height: 32) } private var progressBar: some View { @@ -98,7 +99,7 @@ struct DownloadHistoryRow: View { .frame(height: 3) Capsule() - .fill(Color.blue) + .fill(theme.accent) .frame(width: geo.size.width * download.displayProgress, height: 3) .animation(.easeOut(duration: 0.2), value: download.displayProgress) } @@ -119,7 +120,7 @@ struct DownloadHistoryRow: View { downloadManager.openDownloadInFinder(download) } case .failed, .cancelled: - iconButton("arrow.clockwise", color: .blue) { + iconButton("arrow.clockwise", color: theme.accent) { downloadManager.retryDownload(download) } default: @@ -153,36 +154,25 @@ struct DownloadHistoryRow: View { return url.host?.replacingOccurrences(of: "www.", with: "") } - private var fileIcon: String { - let ext = (download.fileName as NSString).pathExtension.lowercased() - switch ext { - case "pdf": return "doc.fill" - case "zip", "rar", "7z", "tar", "gz": return "archivebox.fill" - case "jpg", "jpeg", "png", "gif", "webp", "svg": return "photo.fill" - case "mp4", "mov", "avi", "mkv", "webm": return "video.fill" - case "mp3", "wav", "flac", "aac", "ogg": return "music.note" - case "dmg", "pkg", "app": return "app.fill" - case "html", "htm": return "globe" - case "txt", "md", "rtf": return "doc.text.fill" - case "json", "xml", "csv": return "tablecells.fill" - case "swift", "js", "py", "rb", "go": return "chevron.left.forwardslash.chevron.right" - default: return "doc.fill" + /// Returns the native macOS file icon for this download, matching what Finder shows. + private var nativeFileIcon: NSImage { + // For completed downloads with a file on disk, get the icon from the actual file + if let url = download.destinationURL, + FileManager.default.fileExists(atPath: url.path) + { + return NSWorkspace.shared.icon(forFile: url.path) } - } - - private var iconColor: Color { - switch download.status { - case .downloading: return .blue - case .completed: return .green - case .failed: return .red - case .cancelled: return .orange - default: return .gray + // Otherwise derive from the file extension via UTType + let ext = (download.fileName as NSString).pathExtension + if !ext.isEmpty, let utType = UTType(filenameExtension: ext) { + return NSWorkspace.shared.icon(for: utType) } + return NSWorkspace.shared.icon(for: .data) } private var statusColor: Color { switch download.status { - case .downloading: return .blue + case .downloading: return theme.accent case .failed: return .red case .cancelled: return .orange default: return .secondary diff --git a/ora/Features/Downloads/Views/DownloadsHistoryView.swift b/ora/Features/Downloads/Views/DownloadsHistoryView.swift index 8555ee69..3c024c8c 100644 --- a/ora/Features/Downloads/Views/DownloadsHistoryView.swift +++ b/ora/Features/Downloads/Views/DownloadsHistoryView.swift @@ -47,14 +47,17 @@ struct DownloadsHistoryView: View { Button(action: { downloadManager.clearCompletedDownloads() }) { - Text("Clear") - .font(.system(size: 11, weight: .medium)) - .foregroundColor(.secondary) + HStack(spacing: 4) { + OraIcons(icon: .brush1, size: .md, color: .secondary) + Text("Clear") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.secondary) + }.frame(alignment: .center) } .buttonStyle(.plain) } } - .padding(.horizontal, 10) + .padding(.trailing, 12) .frame(height: 38) } diff --git a/ora/Features/Sidebar/Views/DownloadsWidget.swift b/ora/Features/Sidebar/Views/DownloadsWidget.swift index 0d5a44d1..e5713c96 100644 --- a/ora/Features/Sidebar/Views/DownloadsWidget.swift +++ b/ora/Features/Sidebar/Views/DownloadsWidget.swift @@ -5,51 +5,56 @@ struct DownloadsWidget: View { @Environment(\.theme) private var theme @State private var isHovered = false + /// Aggregate progress across all active downloads (0...1) + private var totalProgress: Double { + let active = downloadManager.activeDownloads + guard !active.isEmpty else { return 0 } + let total = active.reduce(0.0) { $0 + $1.displayProgress } + return total / Double(active.count) + } + + private var hasActiveDownloads: Bool { + !downloadManager.activeDownloads.isEmpty + } + var body: some View { - VStack(spacing: 0) { - // Active downloads (compact inline progress) - if !downloadManager.activeDownloads.isEmpty { - VStack(spacing: 6) { - ForEach(downloadManager.activeDownloads) { download in - DownloadProgressView(download: download) { - downloadManager.cancelDownload(download) - } - } - } - .padding(.bottom, 8) + Button { + withAnimation(.spring(response: 0.45, dampingFraction: 0.88)) { + downloadManager.isShowingDownloadsHistory.toggle() } + } label: { + ZStack { + // Circular progress ring behind the icon when downloading + if hasActiveDownloads { + Circle() + .stroke(theme.accent.opacity(0.2), lineWidth: 2) + .frame(width: 24, height: 24) - // Downloads button - opens the downloads history destination - Button(action: { - withAnimation(.spring(response: 0.45, dampingFraction: 0.88)) { - downloadManager.isShowingDownloadsHistory.toggle() + Circle() + .trim(from: 0, to: totalProgress) + .stroke(theme.accent, style: StrokeStyle(lineWidth: 2, lineCap: .round)) + .frame(width: 24, height: 24) + .rotationEffect(.degrees(-90)) + .animation(.easeOut(duration: 0.25), value: totalProgress) } - }) { - HStack(spacing: 8) { + + if hasActiveDownloads { Image(systemName: "arrow.down") - .foregroundColor(downloadButtonColor) - .frame(width: 12, height: 12) - } - .padding(8) - .background(isHovered ? theme.invertedSolidWindowBackgroundColor.opacity(0.3) : .clear) - .cornerRadius(8) - } - .buttonStyle(.plain) - .onHover { hovering in - withAnimation(.easeOut(duration: 0.15)) { - isHovered = hovering + .font(.system(size: 12, weight: .bold)) + .foregroundColor(theme.accent) + } else { + OraIcons(icon: .downloadBox, size: .md, color: .secondary) } } + .frame(width: 32, height: 32) + .background(isHovered ? theme.invertedSolidWindowBackgroundColor.opacity(0.3) : .clear) + .cornerRadius(8) } - } - - private var downloadButtonColor: Color { - if !downloadManager.activeDownloads.isEmpty { - return .blue - } else if downloadManager.recentDownloads.contains(where: { $0.status == .completed }) { - return .green - } else { - return .secondary + .buttonStyle(.plain) + .onHover { hovering in + withAnimation(.easeOut(duration: 0.15)) { + isHovered = hovering + } } } } diff --git a/ora/Features/Sidebar/Views/SidebarView.swift b/ora/Features/Sidebar/Views/SidebarView.swift index bd4139cb..e5b8d668 100644 --- a/ora/Features/Sidebar/Views/SidebarView.swift +++ b/ora/Features/Sidebar/Views/SidebarView.swift @@ -62,7 +62,7 @@ struct SidebarView: View { let progress = transitionProgress(for: width) ZStack(alignment: .leading) { - // Spaces content - pushes back when downloads is shown + // Spaces content - pushes back and blurs out when downloads is shown spacesContent .frame(width: width) .offset(x: width * 0.12 * progress) diff --git a/ora/Features/Tabs/Browser/TabBrowserPageDelegate.swift b/ora/Features/Tabs/Browser/TabBrowserPageDelegate.swift index d3c4d462..0a40c258 100644 --- a/ora/Features/Tabs/Browser/TabBrowserPageDelegate.swift +++ b/ora/Features/Tabs/Browser/TabBrowserPageDelegate.swift @@ -195,6 +195,14 @@ final class TabBrowserPageDelegate: BrowserPageDelegate { func browserPage(_ page: BrowserPage, didStartDownload download: BrowserDownloadTask) { MainActor.assumeIsolated { tab?.downloadManager?.handleDownload(download) + + guard page.isDownloadNavigation, let tab else { return } + + if page.lastCommittedURL != nil { + tab.goBack() + } else if let tabManager = tab.tabManager { + tabManager.closeTab(tab: tab) + } } } diff --git a/ora/Shared/Components/Icons/Brush1.swift b/ora/Shared/Components/Icons/Brush1.swift new file mode 100644 index 00000000..5a72af5f --- /dev/null +++ b/ora/Shared/Components/Icons/Brush1.swift @@ -0,0 +1,80 @@ +import SwiftUI + +struct Brush1: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + var strokePath2 = Path() + strokePath2.move(to: CGPoint(x: 0.30977 * width, y: 0.4446 * height)) + strokePath2.addLine(to: CGPoint(x: 0.07425 * width, y: 0.52602 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.16271 * width, y: 0.8089 * height), + control1: CGPoint(x: -0.03701 * width, y: 0.5676 * height), + control2: CGPoint(x: 0.11295 * width, y: 0.75939 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.45459 * width, y: 0.90442 * height), + control1: CGPoint(x: 0.21248 * width, y: 0.85841 * height), + control2: CGPoint(x: 0.4128 * width, y: 1.0151 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.53382 * width, y: 0.66751 * height)) + strokePath2.move(to: CGPoint(x: 0.09018 * width, y: 0.69268 * height)) + strokePath2.addLine(to: CGPoint(x: 0.21298 * width, y: 0.63005 * height)) + strokePath2.move(to: CGPoint(x: 0.29041 * width, y: 0.87485 * height)) + strokePath2.addLine(to: CGPoint(x: 0.34741 * width, y: 0.7638 * height)) + strokePath2.move(to: CGPoint(x: 0.71341 * width, y: 0.35237 * height)) + strokePath2.addLine(to: CGPoint(x: 0.9217 * width, y: 0.13683 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.83953 * width, y: 0.05468 * height), + control1: CGPoint(x: 0.98339 * width, y: 0.07547 * height), + control2: CGPoint(x: 0.89859 * width, y: -0.00409 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.62163 * width, y: 0.26106 * height)) + strokePath2.move(to: CGPoint(x: 0.51708 * width, y: 0.31815 * height)) + strokePath2.addLine(to: CGPoint(x: 0.56264 * width, y: 0.27283 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.62975 * width, y: 0.27617 * height), + control1: CGPoint(x: 0.58056 * width, y: 0.255 * height), + control2: CGPoint(x: 0.61183 * width, y: 0.25834 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.70085 * width, y: 0.3469 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.70085 * width, y: 0.41147 * height), + control1: CGPoint(x: 0.71877 * width, y: 0.36473 * height), + control2: CGPoint(x: 0.71877 * width, y: 0.39364 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.6553 * width, y: 0.45679 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.62973 * width, y: 0.50596 * height), + control1: CGPoint(x: 0.64186 * width, y: 0.47015 * height), + control2: CGPoint(x: 0.63293 * width, y: 0.48734 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.60725 * width, y: 0.63672 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.52957 * width, y: 0.66131 * height), + control1: CGPoint(x: 0.60097 * width, y: 0.67324 * height), + control2: CGPoint(x: 0.5559 * width, y: 0.6875 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.32329 * width, y: 0.4437 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.33844 * width, y: 0.36929 * height), + control1: CGPoint(x: 0.29696 * width, y: 0.4175 * height), + control2: CGPoint(x: 0.30174 * width, y: 0.37554 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.46987 * width, y: 0.34693 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.51708 * width, y: 0.31815 * height), + control1: CGPoint(x: 0.4886 * width, y: 0.34374 * height), + control2: CGPoint(x: 0.50365 * width, y: 0.33151 * height) + ) + strokePath2.closeSubpath() + path.addPath(strokePath2.strokedPath(StrokeStyle( + lineWidth: 0.06818 * width, + lineCap: .round, + lineJoin: .round, + miterLimit: 4 + ))) + return path + } +} diff --git a/ora/Shared/Components/Icons/Brush2.swift b/ora/Shared/Components/Icons/Brush2.swift new file mode 100644 index 00000000..80a115b3 --- /dev/null +++ b/ora/Shared/Components/Icons/Brush2.swift @@ -0,0 +1,117 @@ +import SwiftUI + +struct Brush2: Shape { + // swiftlint:disable:next function_body_length + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + var strokePath2 = Path() + strokePath2.move(to: CGPoint(x: 0.4875 * width, y: 0.53409 * height)) + strokePath2.addLine(to: CGPoint(x: 0.6375 * width, y: 0.625 * height)) + strokePath2.move(to: CGPoint(x: 0.4875 * width, y: 0.53409 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.0375 * width, y: 0.57276 * height), + control1: CGPoint(x: 0.26353 * width, y: 0.6589 * height), + control2: CGPoint(x: 0.13159 * width, y: 0.61504 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.1223 * width, y: 0.80682 * height), + control1: CGPoint(x: 0.0375 * width, y: 0.66726 * height), + control2: CGPoint(x: 0.07156 * width, y: 0.74633 * height) + ) + strokePath2.move(to: CGPoint(x: 0.4875 * width, y: 0.53409 * height)) + strokePath2.addLine(to: CGPoint(x: 0.6375 * width, y: 0.32563 * height)) + strokePath2.move(to: CGPoint(x: 0.6375 * width, y: 0.625 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.39528 * width, y: 0.94318 * height), + control1: CGPoint(x: 0.62655 * width, y: 0.7176 * height), + control2: CGPoint(x: 0.55883 * width, y: 0.90614 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.1223 * width, y: 0.80682 * height), + control1: CGPoint(x: 0.30453 * width, y: 0.94318 * height), + control2: CGPoint(x: 0.19588 * width, y: 0.89453 * height) + ) + strokePath2.move(to: CGPoint(x: 0.6375 * width, y: 0.625 * height)) + strokePath2.addLine(to: CGPoint(x: 0.76739 * width, y: 0.39773 * height)) + strokePath2.move(to: CGPoint(x: 0.1223 * width, y: 0.80682 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.39528 * width, y: 0.76136 * height), + control1: CGPoint(x: 0.17422 * width, y: 0.8144 * height), + control2: CGPoint(x: 0.3015 * width, y: 0.81591 * height) + ) + strokePath2.move(to: CGPoint(x: 0.6375 * width, y: 0.32563 * height)) + strokePath2.addLine(to: CGPoint(x: 0.82788 * width, y: 0.06106 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.90509 * width, y: 0.0397 * height), + control1: CGPoint(x: 0.84323 * width, y: 0.03645 * height), + control2: CGPoint(x: 0.87719 * width, y: 0.02706 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.9313 * width, y: 0.11092 * height), + control1: CGPoint(x: 0.93404 * width, y: 0.05281 * height), + control2: CGPoint(x: 0.94578 * width, y: 0.0847 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.76739 * width, y: 0.39773 * height)) + strokePath2.move(to: CGPoint(x: 0.6375 * width, y: 0.32563 * height)) + strokePath2.addLine(to: CGPoint(x: 0.76739 * width, y: 0.39773 * height)) + strokePath2.move(to: CGPoint(x: 0.1875 * width, y: 0.125 * height)) + strokePath2.addLine(to: CGPoint(x: 0.19855 * width, y: 0.15216 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.23458 * width, y: 0.21856 * height), + control1: CGPoint(x: 0.21305 * width, y: 0.18777 * height), + control2: CGPoint(x: 0.2203 * width, y: 0.20557 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.30763 * width, y: 0.25131 * height), + control1: CGPoint(x: 0.24887 * width, y: 0.23155 * height), + control2: CGPoint(x: 0.26846 * width, y: 0.23814 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.3375 * width, y: 0.26136 * height)) + strokePath2.addLine(to: CGPoint(x: 0.30763 * width, y: 0.27141 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.23458 * width, y: 0.30417 * height), + control1: CGPoint(x: 0.26846 * width, y: 0.28459 * height), + control2: CGPoint(x: 0.24887 * width, y: 0.29118 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.19855 * width, y: 0.37057 * height), + control1: CGPoint(x: 0.2203 * width, y: 0.31716 * height), + control2: CGPoint(x: 0.21305 * width, y: 0.33496 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.1875 * width, y: 0.39773 * height)) + strokePath2.addLine(to: CGPoint(x: 0.17645 * width, y: 0.37057 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.14042 * width, y: 0.30417 * height), + control1: CGPoint(x: 0.16195 * width, y: 0.33496 * height), + control2: CGPoint(x: 0.1547 * width, y: 0.31716 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.06737 * width, y: 0.27141 * height), + control1: CGPoint(x: 0.12613 * width, y: 0.29118 * height), + control2: CGPoint(x: 0.10654 * width, y: 0.28459 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.0375 * width, y: 0.26136 * height)) + strokePath2.addLine(to: CGPoint(x: 0.06737 * width, y: 0.25131 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.14042 * width, y: 0.21856 * height), + control1: CGPoint(x: 0.10654 * width, y: 0.23814 * height), + control2: CGPoint(x: 0.12613 * width, y: 0.23155 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.17645 * width, y: 0.15216 * height), + control1: CGPoint(x: 0.1547 * width, y: 0.20557 * height), + control2: CGPoint(x: 0.16195 * width, y: 0.18777 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.1875 * width, y: 0.125 * height)) + strokePath2.closeSubpath() + path.addPath(strokePath2.strokedPath(StrokeStyle( + lineWidth: 0.075 * width, + lineCap: .round, + lineJoin: .round, + miterLimit: 4 + ))) + return path + } +} diff --git a/ora/Shared/Components/Icons/DownloadBox.swift b/ora/Shared/Components/Icons/DownloadBox.swift new file mode 100644 index 00000000..98ac35f6 --- /dev/null +++ b/ora/Shared/Components/Icons/DownloadBox.swift @@ -0,0 +1,80 @@ +import SwiftUI + +struct DownloadBox: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + var strokePath2 = Path() + strokePath2.move(to: CGPoint(x: 0.03571 * width, y: 0.22619 * height)) + strokePath2.addLine(to: CGPoint(x: 0.03571 * width, y: 0.55952 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.0915 * width, y: 0.88469 * height), + control1: CGPoint(x: 0.03571 * width, y: 0.7391 * height), + control2: CGPoint(x: 0.03571 * width, y: 0.8289 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.41667 * width, y: 0.94048 * height), + control1: CGPoint(x: 0.14729 * width, y: 0.94048 * height), + control2: CGPoint(x: 0.23708 * width, y: 0.94048 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.55952 * width, y: 0.94048 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.88469 * width, y: 0.88469 * height), + control1: CGPoint(x: 0.73911 * width, y: 0.94048 * height), + control2: CGPoint(x: 0.8289 * width, y: 0.94048 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.94048 * width, y: 0.55952 * height), + control1: CGPoint(x: 0.94048 * width, y: 0.8289 * height), + control2: CGPoint(x: 0.94048 * width, y: 0.7391 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.94048 * width, y: 0.22619 * height)) + strokePath2.move(to: CGPoint(x: 0.03571 * width, y: 0.22619 * height)) + strokePath2.addLine(to: CGPoint(x: 0.94048 * width, y: 0.22619 * height)) + strokePath2.move(to: CGPoint(x: 0.03571 * width, y: 0.22619 * height)) + strokePath2.addLine(to: CGPoint(x: 0.06429 * width, y: 0.1881 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.18863 * width, y: 0.05582 * height), + control1: CGPoint(x: 0.12037 * width, y: 0.11332 * height), + control2: CGPoint(x: 0.14841 * width, y: 0.07593 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.36905 * width, y: 0.03571 * height), + control1: CGPoint(x: 0.22884 * width, y: 0.03571 * height), + control2: CGPoint(x: 0.27558 * width, y: 0.03571 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.60714 * width, y: 0.03571 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.78757 * width, y: 0.05582 * height), + control1: CGPoint(x: 0.70061 * width, y: 0.03571 * height), + control2: CGPoint(x: 0.74735 * width, y: 0.03571 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.9119 * width, y: 0.1881 * height), + control1: CGPoint(x: 0.82779 * width, y: 0.07593 * height), + control2: CGPoint(x: 0.85582 * width, y: 0.11332 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.94048 * width, y: 0.22619 * height)) + strokePath2.move(to: CGPoint(x: 0.63095 * width, y: 0.60714 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.4881 * width, y: 0.75 * height), + control1: CGPoint(x: 0.63095 * width, y: 0.60714 * height), + control2: CGPoint(x: 0.52574 * width, y: 0.75 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.34524 * width, y: 0.60714 * height), + control1: CGPoint(x: 0.45045 * width, y: 0.75 * height), + control2: CGPoint(x: 0.34524 * width, y: 0.60714 * height) + ) + strokePath2.move(to: CGPoint(x: 0.4881 * width, y: 0.72619 * height)) + strokePath2.addLine(to: CGPoint(x: 0.4881 * width, y: 0.41667 * height)) + path.addPath(strokePath2.strokedPath(StrokeStyle( + lineWidth: 0.07143 * width, + lineCap: .round, + lineJoin: .round, + miterLimit: 4 + ))) + return path + } +} diff --git a/ora/Shared/Components/Icons/DownloadBox2.swift b/ora/Shared/Components/Icons/DownloadBox2.swift new file mode 100644 index 00000000..b85e0808 --- /dev/null +++ b/ora/Shared/Components/Icons/DownloadBox2.swift @@ -0,0 +1,119 @@ +import SwiftUI + +struct DownloadBox2: Shape { + // swiftlint:disable:next function_body_length + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + var strokePath2 = Path() + strokePath2.move(to: CGPoint(x: 0.03571 * width, y: 0.60714 * height)) + strokePath2.addLine(to: CGPoint(x: 0.03571 * width, y: 0.41667 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.0915 * width, y: 0.0915 * height), + control1: CGPoint(x: 0.03571 * width, y: 0.23708 * height), + control2: CGPoint(x: 0.03571 * width, y: 0.14729 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.41667 * width, y: 0.03571 * height), + control1: CGPoint(x: 0.14729 * width, y: 0.03571 * height), + control2: CGPoint(x: 0.23708 * width, y: 0.03571 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.55952 * width, y: 0.03571 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.88469 * width, y: 0.0915 * height), + control1: CGPoint(x: 0.7391 * width, y: 0.03571 * height), + control2: CGPoint(x: 0.8289 * width, y: 0.03571 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.94048 * width, y: 0.41667 * height), + control1: CGPoint(x: 0.94048 * width, y: 0.14729 * height), + control2: CGPoint(x: 0.94048 * width, y: 0.23708 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.94048 * width, y: 0.60714 * height)) + strokePath2.move(to: CGPoint(x: 0.63095 * width, y: 0.60714 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.4881 * width, y: 0.75 * height), + control1: CGPoint(x: 0.63095 * width, y: 0.60714 * height), + control2: CGPoint(x: 0.52574 * width, y: 0.75 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.34524 * width, y: 0.60714 * height), + control1: CGPoint(x: 0.45045 * width, y: 0.75 * height), + control2: CGPoint(x: 0.34524 * width, y: 0.60714 * height) + ) + strokePath2.move(to: CGPoint(x: 0.4881 * width, y: 0.72619 * height)) + strokePath2.addLine(to: CGPoint(x: 0.4881 * width, y: 0.41667 * height)) + strokePath2.move(to: CGPoint(x: 0.58333 * width, y: 0.94048 * height)) + strokePath2.addLine(to: CGPoint(x: 0.39286 * width, y: 0.94048 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.10535 * width, y: 0.89724 * height), + control1: CGPoint(x: 0.23631 * width, y: 0.94048 * height), + control2: CGPoint(x: 0.15804 * width, y: 0.94048 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.07895 * width, y: 0.87084 * height), + control1: CGPoint(x: 0.09571 * width, y: 0.88932 * height), + control2: CGPoint(x: 0.08687 * width, y: 0.88048 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.03571 * width, y: 0.58333 * height), + control1: CGPoint(x: 0.03571 * width, y: 0.81815 * height), + control2: CGPoint(x: 0.03571 * width, y: 0.73988 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.07895 * width, y: 0.29583 * height), + control1: CGPoint(x: 0.03571 * width, y: 0.42679 * height), + control2: CGPoint(x: 0.03571 * width, y: 0.34851 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.10535 * width, y: 0.26943 * height), + control1: CGPoint(x: 0.08687 * width, y: 0.28619 * height), + control2: CGPoint(x: 0.09571 * width, y: 0.27734 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.39286 * width, y: 0.22619 * height), + control1: CGPoint(x: 0.15804 * width, y: 0.22619 * height), + control2: CGPoint(x: 0.23631 * width, y: 0.22619 * height) + ) + strokePath2.addLine(to: CGPoint(x: 0.58333 * width, y: 0.22619 * height)) + strokePath2.addCurve( + to: CGPoint(x: 0.87084 * width, y: 0.26943 * height), + control1: CGPoint(x: 0.73988 * width, y: 0.22619 * height), + control2: CGPoint(x: 0.81815 * width, y: 0.22619 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.89724 * width, y: 0.29583 * height), + control1: CGPoint(x: 0.88048 * width, y: 0.27734 * height), + control2: CGPoint(x: 0.88932 * width, y: 0.28619 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.94048 * width, y: 0.58333 * height), + control1: CGPoint(x: 0.94048 * width, y: 0.34851 * height), + control2: CGPoint(x: 0.94048 * width, y: 0.42679 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.89724 * width, y: 0.87084 * height), + control1: CGPoint(x: 0.94048 * width, y: 0.73988 * height), + control2: CGPoint(x: 0.94048 * width, y: 0.81815 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.87084 * width, y: 0.89724 * height), + control1: CGPoint(x: 0.88932 * width, y: 0.88048 * height), + control2: CGPoint(x: 0.88048 * width, y: 0.88932 * height) + ) + strokePath2.addCurve( + to: CGPoint(x: 0.58333 * width, y: 0.94048 * height), + control1: CGPoint(x: 0.81815 * width, y: 0.94048 * height), + control2: CGPoint(x: 0.73988 * width, y: 0.94048 * height) + ) + strokePath2.closeSubpath() + path.addPath(strokePath2.strokedPath(StrokeStyle( + lineWidth: 0.07143 * width, + lineCap: .round, + lineJoin: .round, + miterLimit: 4 + ))) + return path + } +} diff --git a/ora/Shared/Components/Icons/OraIcon.swift b/ora/Shared/Components/Icons/OraIcon.swift index 189c7cbb..9274a0be 100644 --- a/ora/Shared/Components/Icons/OraIcon.swift +++ b/ora/Shared/Components/Icons/OraIcon.swift @@ -9,7 +9,7 @@ enum OraIconSize { var dimension: CGFloat { switch self { case .xs: 10 - case .sm: 12 + case .sm: 14 case .md: 16 case .lg: 20 case .xl: 24 @@ -42,6 +42,10 @@ enum OraIconType { case spaceCardsEdit case autofill case copy + case brush1 + case brush2 + case downloadBox + case downloadBox2 case custom(AnyOraShape) var shape: AnyOraShape { @@ -53,6 +57,10 @@ enum OraIconType { case .spaceCardsEdit: AnyOraShape(SpaceCardsEditIcon()) case .autofill: AnyOraShape(AutofillIcon()) case .copy: AnyOraShape(CopyIcon()) + case .brush1: AnyOraShape(Brush1()) + case .brush2: AnyOraShape(Brush2()) + case .downloadBox: AnyOraShape(DownloadBox()) + case .downloadBox2: AnyOraShape(DownloadBox2()) case let .custom(shape): shape } } From b3327904420b4af8ff2efcbef8312da6a229d021 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sat, 14 Mar 2026 21:16:54 +0300 Subject: [PATCH 4/5] feat: unified action menu for download rows Replace multiple hover action buttons with a single ellipsis menu containing all actions (open, show in Finder, copy path, retry, move to trash, remove from Ora). Add moveToTrash to DownloadManager. Make downloads history view fill sidebar edge to edge. --- .../Downloads/Services/DownloadManager.swift | 8 + .../Downloads/Views/DownloadHistoryRow.swift | 152 +++++++++--------- .../Views/DownloadsHistoryView.swift | 9 +- .../Sidebar/Views/FloatingSidebar.swift | 2 +- ora/Features/Sidebar/Views/SidebarView.swift | 1 - 5 files changed, 85 insertions(+), 87 deletions(-) diff --git a/ora/Features/Downloads/Services/DownloadManager.swift b/ora/Features/Downloads/Services/DownloadManager.swift index c794a482..5e237fc9 100644 --- a/ora/Features/Downloads/Services/DownloadManager.swift +++ b/ora/Features/Downloads/Services/DownloadManager.swift @@ -211,6 +211,14 @@ class DownloadManager: ObservableObject { NSWorkspace.shared.open(url) } + /// Moves the downloaded file to Trash and removes the entry from history + func moveToTrash(_ download: Download) { + if let url = download.destinationURL { + try? FileManager.default.trashItem(at: url, resultingItemURL: nil) + } + deleteDownload(download) + } + /// Re-opens the original URL in the browser to re-trigger the download func retryDownload(_ download: Download) { guard let url = URL(string: download.originalURLString) else { return } diff --git a/ora/Features/Downloads/Views/DownloadHistoryRow.swift b/ora/Features/Downloads/Views/DownloadHistoryRow.swift index f7df6dca..594ee5f8 100644 --- a/ora/Features/Downloads/Views/DownloadHistoryRow.swift +++ b/ora/Features/Downloads/Views/DownloadHistoryRow.swift @@ -56,8 +56,8 @@ struct DownloadHistoryRow: View { } } - if isHovered || download.status == .downloading { - actionButtons + if isHovered { + moreMenuButton } } .padding(.horizontal, 6) @@ -78,7 +78,7 @@ struct DownloadHistoryRow: View { } } .contextMenu { - DownloadHistoryContextMenu(download: download) + downloadMenuItems } } @@ -108,43 +108,82 @@ struct DownloadHistoryRow: View { .padding(.top, 2) } - private var actionButtons: some View { - HStack(spacing: 2) { - switch download.status { - case .downloading: - iconButton("xmark.circle.fill", color: .secondary) { - downloadManager.cancelDownload(download) - } - case .completed: - iconButton("folder", color: .secondary) { - downloadManager.openDownloadInFinder(download) - } - case .failed, .cancelled: - iconButton("arrow.clockwise", color: theme.accent) { - downloadManager.retryDownload(download) + private var moreMenuButton: some View { + Menu { + downloadMenuItems + } label: { + Image(systemName: "ellipsis") + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.secondary) + .frame(width: 20, height: 20) + } + .menuStyle(.borderlessButton) + .menuIndicator(.hidden) + .fixedSize() + } + + @ViewBuilder + private var downloadMenuItems: some View { + if download.status == .completed { + Button { + downloadManager.openFile(download) + } label: { + Label("Open", systemImage: "arrow.up.doc") + } + + Button { + downloadManager.openDownloadInFinder(download) + } label: { + Label("Show in Finder", systemImage: "folder") + } + + Button { + if let path = download.destinationURL?.path { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(path, forType: .string) } - default: - EmptyView() + } label: { + Label("Copy Path", systemImage: "doc.on.doc") } - if download.status != .downloading { - iconButton("xmark", color: .secondary) { - withAnimation(.easeOut(duration: 0.2)) { - downloadManager.deleteDownload(download) - } + Divider() + + Button(role: .destructive) { + withAnimation(.easeOut(duration: 0.2)) { + downloadManager.moveToTrash(download) } + } label: { + Label("Move to Trash", systemImage: "trash") } } - } - private func iconButton(_ systemName: String, color: Color, action: @escaping () -> Void) -> some View { - Button(action: action) { - Image(systemName: systemName) - .font(.system(size: 11)) - .foregroundColor(color) - .frame(width: 20, height: 20) + if download.status == .downloading { + Button(role: .destructive) { + downloadManager.cancelDownload(download) + } label: { + Label("Cancel Download", systemImage: "xmark.circle") + } + } + + if download.status == .failed || download.status == .cancelled { + Button { + downloadManager.retryDownload(download) + } label: { + Label("Retry Download", systemImage: "arrow.clockwise") + } + } + + if download.status != .downloading { + Divider() + + Button { + withAnimation(.easeOut(duration: 0.2)) { + downloadManager.deleteDownload(download) + } + } label: { + Label("Remove from Ora", systemImage: "minus.circle") + } } - .buttonStyle(.plain) } // MARK: - Computed Properties @@ -156,13 +195,11 @@ struct DownloadHistoryRow: View { /// Returns the native macOS file icon for this download, matching what Finder shows. private var nativeFileIcon: NSImage { - // For completed downloads with a file on disk, get the icon from the actual file if let url = download.destinationURL, FileManager.default.fileExists(atPath: url.path) { return NSWorkspace.shared.icon(forFile: url.path) } - // Otherwise derive from the file extension via UTType let ext = (download.fileName as NSString).pathExtension if !ext.isEmpty, let utType = UTType(filenameExtension: ext) { return NSWorkspace.shared.icon(for: utType) @@ -183,7 +220,8 @@ struct DownloadHistoryRow: View { switch download.status { case .downloading: if download.displayFileSize > 0 { - return "\(download.formattedDownloadedSize) of \(download.formattedFileSize) \u{00B7} \(Int(download.displayProgress * 100))%" + let pct = Int(download.displayProgress * 100) + return "\(download.formattedDownloadedSize) of \(download.formattedFileSize) \u{00B7} \(pct)%" } return download.formattedDownloadedSize case .completed: @@ -203,47 +241,3 @@ struct DownloadHistoryRow: View { return formatter.localizedString(for: date, relativeTo: Date()) } } - -struct DownloadHistoryContextMenu: View { - let download: Download - @EnvironmentObject var downloadManager: DownloadManager - - var body: some View { - Group { - if download.status == .completed { - Button("Open") { - downloadManager.openFile(download) - } - - Button("Show in Finder") { - downloadManager.openDownloadInFinder(download) - } - - Button("Copy Path") { - if let path = download.destinationURL?.path { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(path, forType: .string) - } - } - } - - if download.status == .downloading { - Button("Cancel Download") { - downloadManager.cancelDownload(download) - } - } - - if download.status == .failed || download.status == .cancelled { - Button("Retry Download") { - downloadManager.retryDownload(download) - } - } - - Divider() - - Button("Remove from History") { - downloadManager.deleteDownload(download) - } - } - } -} diff --git a/ora/Features/Downloads/Views/DownloadsHistoryView.swift b/ora/Features/Downloads/Views/DownloadsHistoryView.swift index 3c024c8c..1cbdc44f 100644 --- a/ora/Features/Downloads/Views/DownloadsHistoryView.swift +++ b/ora/Features/Downloads/Views/DownloadsHistoryView.swift @@ -16,7 +16,6 @@ struct DownloadsHistoryView: View { VStack(alignment: .leading, spacing: 0) { header searchBar - Divider().opacity(0.5) content Spacer(minLength: 0) footer @@ -67,8 +66,7 @@ struct DownloadsHistoryView: View { OraInput( text: $searchText, placeholder: "Search files...", - variant: .ghost, - size: .sm, + size: .md, leadingIcon: "magnifyingglass" ) .padding(.horizontal, 10) @@ -92,8 +90,8 @@ struct DownloadsHistoryView: View { Spacer() } - .padding(.horizontal, 10) - .padding(.bottom, 10) + .padding(.horizontal, 16) + .padding(.vertical, 16) } // MARK: - Content @@ -246,7 +244,6 @@ struct DownloadsHistoryView: View { Text(title) .font(.system(size: 11, weight: .medium)) .foregroundColor(.secondary) - .textCase(.uppercase) .padding(.horizontal, 6) .padding(.top, 10) .padding(.bottom, 4) diff --git a/ora/Features/Sidebar/Views/FloatingSidebar.swift b/ora/Features/Sidebar/Views/FloatingSidebar.swift index 8958543e..4d3ecff4 100644 --- a/ora/Features/Sidebar/Views/FloatingSidebar.swift +++ b/ora/Features/Sidebar/Views/FloatingSidebar.swift @@ -7,7 +7,7 @@ struct FloatingSidebar: View { if #available(macOS 26, *) { return 13 } else { - return 6 + return 5 } }() diff --git a/ora/Features/Sidebar/Views/SidebarView.swift b/ora/Features/Sidebar/Views/SidebarView.swift index e5b8d668..ea358585 100644 --- a/ora/Features/Sidebar/Views/SidebarView.swift +++ b/ora/Features/Sidebar/Views/SidebarView.swift @@ -73,7 +73,6 @@ struct SidebarView: View { // Downloads history - slides in from leading edge DownloadsHistoryView() .frame(width: width) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) .offset(x: -width + width * progress) .shadow(color: .black.opacity(0.08 * Double(progress)), radius: 8, x: 4, y: 0) .allowsHitTesting(progress >= 0.5) From 7e67ed30d2ec121fbe6621ba5de559cae6ba9e93 Mon Sep 17 00:00:00 2001 From: Yonathan Dejene Date: Sat, 14 Mar 2026 21:25:50 +0300 Subject: [PATCH 5/5] Update ora/Features/Downloads/Views/DownloadsHistoryView.swift Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- ora/Features/Downloads/Views/DownloadsHistoryView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ora/Features/Downloads/Views/DownloadsHistoryView.swift b/ora/Features/Downloads/Views/DownloadsHistoryView.swift index 1cbdc44f..b3a49825 100644 --- a/ora/Features/Downloads/Views/DownloadsHistoryView.swift +++ b/ora/Features/Downloads/Views/DownloadsHistoryView.swift @@ -44,7 +44,7 @@ struct DownloadsHistoryView: View { if hasNonActiveDownloads { Button(action: { - downloadManager.clearCompletedDownloads() + downloadManager.clearNonActiveDownloads() }) { HStack(spacing: 4) { OraIcons(icon: .brush1, size: .md, color: .secondary)