diff --git a/boringNotch/components/Shelf/ViewModels/ShelfStateViewModel.swift b/boringNotch/components/Shelf/ViewModels/ShelfStateViewModel.swift index 5f950b7f..b20c79b8 100644 --- a/boringNotch/components/Shelf/ViewModels/ShelfStateViewModel.swift +++ b/boringNotch/components/Shelf/ViewModels/ShelfStateViewModel.swift @@ -57,6 +57,14 @@ final class ShelfStateViewModel: ObservableObject { items.removeAll { $0.id == item.id } } + func clearAll() { + // Clean up all stored data before clearing + for item in items { + item.cleanupStoredData() + } + items.removeAll() + } + func updateBookmark(for item: ShelfItem, bookmark: Data) { guard let idx = items.firstIndex(where: { $0.id == item.id }) else { return } if case .file = items[idx].kind { diff --git a/boringNotch/components/Shelf/Views/ShelfView.swift b/boringNotch/components/Shelf/Views/ShelfView.swift index 6af023f3..0abceefc 100644 --- a/boringNotch/components/Shelf/Views/ShelfView.swift +++ b/boringNotch/components/Shelf/Views/ShelfView.swift @@ -3,6 +3,7 @@ // boringNotch // // Created by Alexander on 2025-09-24. +// Edited by Ben Lloyd on 2026-02-08. // import SwiftUI @@ -75,6 +76,14 @@ struct ShelfView: View { } .contentShape(Rectangle()) .onTapGesture { selection.clear() } + .contextMenu { + if !tvm.isEmpty { + Button(NSLocalizedString("Clear All", comment: "Button to clear all files from shelf")) { + tvm.clearAll() + selection.clear() + } + } + } } var content: some View { @@ -93,18 +102,47 @@ struct ShelfView: View { .fontWeight(.medium) } } else { - ScrollView(.horizontal) { - LazyHStack(spacing: spacing) { - ForEach(tvm.items) { item in - ShelfItemView(item: item) - .environmentObject(quickLookService) + ZStack(alignment: .topTrailing) { + ScrollView(.horizontal) { + LazyHStack(spacing: spacing) { + ForEach(tvm.items) { item in + ShelfItemView(item: item) + .environmentObject(quickLookService) + } } } - } - .padding(-spacing) - .scrollIndicators(.never) - .onDrop(of: [.fileURL, .url, .utf8PlainText, .plainText, .data], isTargeted: $vm.dragDetectorTargeting) { providers in - handleDrop(providers: providers) + .padding(-spacing) + .scrollIndicators(.never) + .onDrop(of: [.fileURL, .url, .utf8PlainText, .plainText, .data], isTargeted: $vm.dragDetectorTargeting) { providers in + handleDrop(providers: providers) + } + + // Clear All button + Button(action: { + withAnimation { + tvm.clearAll() + selection.clear() + } + }) { + HStack(spacing: 4) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 11)) + Text(NSLocalizedString("Clear All", comment: "Button to clear all files from shelf")) + .font(.system(size: 11, weight: .medium)) + } + .foregroundStyle(.white.opacity(0.7)) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + Capsule() + .fill(.black.opacity(0.4)) + .overlay( + Capsule() + .strokeBorder(Color.white.opacity(0.15), lineWidth: 0.5) + ) + ) + } + .buttonStyle(.plain) } } } diff --git a/boringNotch/components/Tabs/TabSelectionView.swift b/boringNotch/components/Tabs/TabSelectionView.swift index b99d4af1..4f5a744a 100644 --- a/boringNotch/components/Tabs/TabSelectionView.swift +++ b/boringNotch/components/Tabs/TabSelectionView.swift @@ -21,32 +21,48 @@ let tabs = [ struct TabSelectionView: View { @ObservedObject var coordinator = BoringViewCoordinator.shared + @StateObject var shelfVM = ShelfStateViewModel.shared @Namespace var animation var body: some View { - HStack(spacing: 0) { - ForEach(tabs) { tab in - TabButton(label: tab.label, icon: tab.icon, selected: coordinator.currentView == tab.view) { - withAnimation(.smooth) { - coordinator.currentView = tab.view + ZStack(alignment: .topTrailing) { + HStack(spacing: 0) { + ForEach(tabs) { tab in + ZStack(alignment: .topTrailing) { + TabButton(label: tab.label, icon: tab.icon, selected: coordinator.currentView == tab.view) { + withAnimation(.smooth) { + coordinator.currentView = tab.view + } } - } - .frame(height: 26) - .foregroundStyle(tab.view == coordinator.currentView ? .white : .gray) - .background { - if tab.view == coordinator.currentView { - Capsule() - .fill(coordinator.currentView == tab.view ? Color(nsColor: .secondarySystemFill) : Color.clear) - .matchedGeometryEffect(id: "capsule", in: animation) - } else { - Capsule() - .fill(coordinator.currentView == tab.view ? Color(nsColor: .secondarySystemFill) : Color.clear) - .matchedGeometryEffect(id: "capsule", in: animation) - .hidden() + .frame(height: 26) + .foregroundStyle(tab.view == coordinator.currentView ? .white : .gray) + .background { + if tab.view == coordinator.currentView { + Capsule() + .fill(coordinator.currentView == tab.view ? Color(nsColor: .secondarySystemFill) : Color.clear) + .matchedGeometryEffect(id: "capsule", in: animation) + } else { + Capsule() + .fill(coordinator.currentView == tab.view ? Color(nsColor: .secondarySystemFill) : Color.clear) + .matchedGeometryEffect(id: "capsule", in: animation) + .hidden() + } } } + } + } + .clipShape(Capsule()) + + // Badge for shelf file count + if !shelfVM.isEmpty { + Text("\(shelfVM.items.count)") + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(.white) + .frame(width: 18, height: 18) + .background(Color.accentColor) + .clipShape(Circle()) + .offset(x: 5, y: -5) } } - .clipShape(Capsule()) } }