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/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..5e237fc9 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 @@ -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 @@ -201,6 +206,28 @@ class DownloadManager: ObservableObject { NSWorkspace.shared.selectFile(destinationURL.path, inFileViewerRootedAtPath: "") } + func openFile(_ download: Download) { + guard let url = download.destinationURL else { return } + 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 } + 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..594ee5f8 --- /dev/null +++ b/ora/Features/Downloads/Views/DownloadHistoryRow.swift @@ -0,0 +1,243 @@ +import SwiftUI +import UniformTypeIdentifiers + +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) + .truncationMode(.tail) + + if !statusText.isEmpty { + Text("\u{00B7}") + .font(.system(size: 10)) + .foregroundColor(.secondary) + .layoutPriority(1) + } + } + + Text(statusText) + .font(.system(size: 10)) + .foregroundColor(statusColor) + .lineLimit(1) + .truncationMode(.tail) + + Spacer(minLength: 0) + + if download.status == .completed { + Text(download.formattedFileSize) + .font(.system(size: 10)) + .foregroundColor(.secondary) + .lineLimit(1) + .fixedSize() + } + } + + if download.status == .downloading { + progressBar + } + } + + if isHovered { + moreMenuButton + } + } + .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 { + downloadMenuItems + } + } + + // MARK: - Subviews + + private var fileIconView: some View { + Image(nsImage: nativeFileIcon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 32, height: 32) + } + + private var progressBar: some View { + GeometryReader { geo in + ZStack(alignment: .leading) { + Capsule() + .fill(theme.mutedBackground) + .frame(height: 3) + + Capsule() + .fill(theme.accent) + .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 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) + } + } label: { + Label("Copy Path", systemImage: "doc.on.doc") + } + + Divider() + + Button(role: .destructive) { + withAnimation(.easeOut(duration: 0.2)) { + downloadManager.moveToTrash(download) + } + } label: { + Label("Move to Trash", systemImage: "trash") + } + } + + 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") + } + } + } + + // MARK: - Computed Properties + + private var sourceHostname: String? { + guard let url = URL(string: download.originalURLString) else { return nil } + return url.host?.replacingOccurrences(of: "www.", with: "") + } + + /// Returns the native macOS file icon for this download, matching what Finder shows. + private var nativeFileIcon: NSImage { + if let url = download.destinationURL, + FileManager.default.fileExists(atPath: url.path) + { + return NSWorkspace.shared.icon(forFile: url.path) + } + 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 theme.accent + case .failed: return .red + case .cancelled: return .orange + default: return .secondary + } + } + + private var statusText: String { + switch download.status { + case .downloading: + if download.displayFileSize > 0 { + let pct = Int(download.displayProgress * 100) + return "\(download.formattedDownloadedSize) of \(download.formattedFileSize) \u{00B7} \(pct)%" + } + 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()) + } +} diff --git a/ora/Features/Downloads/Views/DownloadsHistoryView.swift b/ora/Features/Downloads/Views/DownloadsHistoryView.swift new file mode 100644 index 00000000..b3a49825 --- /dev/null +++ b/ora/Features/Downloads/Views/DownloadsHistoryView.swift @@ -0,0 +1,263 @@ +import SwiftUI + +struct DownloadsHistoryView: View { + @EnvironmentObject var downloadManager: DownloadManager + @EnvironmentObject var sidebarManager: SidebarManager + @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 + 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.clearNonActiveDownloads() + }) { + 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(.trailing, 12) + .frame(height: 38) + } + + // MARK: - Search Bar + + private var searchBar: some View { + OraInput( + text: $searchText, + placeholder: "Search files...", + size: .md, + leadingIcon: "magnifyingglass" + ) + .padding(.horizontal, 10) + .padding(.vertical, 4) + } + + // 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, 16) + .padding(.vertical, 16) + } + + // MARK: - Content + + @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) { + if !filteredActive.isEmpty { + sectionHeader("Active") + ForEach(filteredActive) { download in + DownloadHistoryRow(download: download) + } + } + + ForEach(groupedHistory, id: \.label) { group in + if !filteredActive.isEmpty || group.label != groupedHistory.first?.label { + Divider().opacity(0.3).padding(.vertical, 4) + } + sectionHeader(group.label) + ForEach(group.downloads) { 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: - 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)) + .foregroundColor(.secondary) + .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..e5713c96 100644 --- a/ora/Features/Sidebar/Views/DownloadsWidget.swift +++ b/ora/Features/Sidebar/Views/DownloadsWidget.swift @@ -5,73 +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 - 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) + + 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) + } - // Downloads button - Button(action: { - downloadManager.isDownloadsPopoverOpen.toggle() - }) { - HStack(spacing: 8) { + if hasActiveDownloads { 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) - .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) } } - .popover(isPresented: $downloadManager.isDownloadsPopoverOpen, arrowEdge: .bottom) { - DownloadsListView() - .environmentObject(downloadManager) - } + .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/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 91d8758d..ea358585 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,98 @@ 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 and blurs out 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) + .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 +204,6 @@ struct SidebarView: View { .onTapGesture(count: 2) { toggleMaximizeWindow() } - .enableInjection() } private func onContainerSelected(container: TabContainer) { 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 } }