Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
58 changes: 48 additions & 10 deletions boringNotch/components/Shelf/Views/ShelfView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// boringNotch
//
// Created by Alexander on 2025-09-24.
// Edited by Ben Lloyd on 2026-02-08.
//

import SwiftUI
Expand Down Expand Up @@ -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 {
Expand All @@ -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: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this. given that it is always floating in the file view, we may want some kind of confirmation flow to prevent accidental deletion, which seems quite easy, and we may also want this floating button to be enabled/disabled by a setting in the shelf settings.

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)
}
}
}
Expand Down
54 changes: 35 additions & 19 deletions boringNotch/components/Tabs/TabSelectionView.swift
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tab selection is intended to be generalized, don't hardcode shelf state and shelf UI into the TabSelectionView

Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Badge is also clipped some of the time. I know this has more to do with a layout issue (I think that shouldn't depend on closed notch height does). But until that is resolved, the badge won't show up properly for everyone and shouldn't be added. It also might be nice if this badge was optional.

Image

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())
}
}

Expand Down