Somewhat working file tray

This commit is contained in:
Hariharan Mudaliar 2025-12-08 13:29:25 +05:30
commit 30422c3d95
29 changed files with 4032 additions and 65 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,87 @@
//
// ShelfSelectionModel.swift
// boringNotch
//
// Created by Alexander on 2025-09-26.
//
import Foundation
import Combine
private let _shelfTypeAnchor: Bool = {
_ = String(describing: ShelfItem.self)
return true
}()
@MainActor
final class ShelfSelectionModel: ObservableObject {
static let shared = ShelfSelectionModel()
@Published private(set) var selectedIDs: Set<UUID> = []
// Anchor for shift-range selection
private var lastAnchorID: UUID? = nil
func isSelected(_ id: UUID) -> Bool { selectedIDs.contains(id) }
var hasSelection: Bool { !selectedIDs.isEmpty }
var firstSelectedItem: ShelfItem? {
guard let firstID = selectedIDs.first else { return nil }
return ShelfStateViewModel.shared.items.first(where: { $0.id == firstID })
}
func selectedItems(in allItems: [ShelfItem]) -> [ShelfItem] {
allItems.filter { selectedIDs.contains($0.id) }
}
func selectSingle(_ item: ShelfItem) {
selectedIDs = [item.id]
lastAnchorID = item.id
}
func toggle(_ item: ShelfItem) {
if selectedIDs.contains(item.id) {
selectedIDs.remove(item.id)
} else {
selectedIDs.insert(item.id)
}
lastAnchorID = item.id
}
func shiftSelect(to item: ShelfItem, in allItems: [ShelfItem]) {
// Determine anchor
let anchorID = lastAnchorID ?? selectedIDs.first ?? item.id
guard let startIndex = allItems.firstIndex(where: { $0.id == anchorID }),
let endIndex = allItems.firstIndex(where: { $0.id == item.id }) else {
// Fallback to single select if indices not found
return selectSingle(item)
}
let lower = min(startIndex, endIndex)
let upper = max(startIndex, endIndex)
let rangeIDs = allItems[lower...upper].map { $0.id }
selectedIDs = Set(rangeIDs)
}
func clear() {
selectedIDs.removeAll()
lastAnchorID = nil
}
// Keep anchor sane if items array changed drastically (optional helper)
func ensureValidAnchor(in allItems: [ShelfItem]) {
if let anchor = lastAnchorID, !allItems.contains(where: { $0.id == anchor }) {
lastAnchorID = selectedIDs.first
}
}
@Published private(set) var isDragging: Bool = false
func beginDrag() {
isDragging = true
}
func endDrag() {
isDragging = false
}
}

View file

@ -0,0 +1,143 @@
//
// ShelfStateViewModel.swift
// boringNotch
//
// Created by Alexander on 2025-10-09.
import Foundation
import AppKit
@MainActor
final class ShelfStateViewModel: ObservableObject {
static let shared = ShelfStateViewModel()
@Published private(set) var items: [ShelfItem] = [] {
didSet { ShelfPersistenceService.shared.save(items) }
}
@Published var isLoading: Bool = false
var isEmpty: Bool { items.isEmpty }
// Queue for deferred bookmark updates to avoid publishing during view updates
private var pendingBookmarkUpdates: [ShelfItem.ID: Data] = [:]
private var updateTask: Task<Void, Never>?
private init() {
items = ShelfPersistenceService.shared.load()
}
func add(_ newItems: [ShelfItem]) {
guard !newItems.isEmpty else { return }
var merged = items
// Deduplicate by identityKey while preserving order (existing first)
var seen: Set<String> = Set(merged.map { $0.identityKey })
for it in newItems {
let key = it.identityKey
if !seen.contains(key) {
merged.append(it)
seen.insert(key)
}
}
items = merged
}
func remove(_ item: ShelfItem) {
item.cleanupStoredData()
items.removeAll { $0.id == item.id }
}
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 {
items[idx].kind = .file(bookmark: bookmark)
}
}
private func scheduleDeferredBookmarkUpdate(for item: ShelfItem, bookmark: Data) {
pendingBookmarkUpdates[item.id] = bookmark
// Cancel existing task and schedule a new one
updateTask?.cancel()
updateTask = Task { @MainActor [weak self] in
await Task.yield()
guard let self = self else { return }
for (itemID, bookmarkData) in self.pendingBookmarkUpdates {
if let idx = self.items.firstIndex(where: { $0.id == itemID }),
case .file = self.items[idx].kind {
self.items[idx].kind = .file(bookmark: bookmarkData)
}
}
self.pendingBookmarkUpdates.removeAll()
}
}
func load(_ providers: [NSItemProvider]) {
guard !providers.isEmpty else { return }
isLoading = true
Task { [weak self] in
let dropped = await ShelfDropService.items(from: providers)
await MainActor.run {
self?.add(dropped)
self?.isLoading = false
}
}
}
func cleanupInvalidItems() {
Task { [weak self] in
guard let self else { return }
var keep: [ShelfItem] = []
for item in self.items {
switch item.kind {
case .file(let data):
let bookmark = Bookmark(data: data)
if await bookmark.validate() {
keep.append(item)
} else {
item.cleanupStoredData()
}
default:
keep.append(item)
}
}
await MainActor.run { self.items = keep }
}
}
func resolveFileURL(for item: ShelfItem) -> URL? {
guard case .file(let bookmarkData) = item.kind else { return nil }
let bookmark = Bookmark(data: bookmarkData)
let result = bookmark.resolve()
if let refreshed = result.refreshedData, refreshed != bookmarkData {
NSLog("Bookmark for \(item) stale; refreshing")
scheduleDeferredBookmarkUpdate(for: item, bookmark: refreshed)
}
return result.url
}
func resolveAndUpdateBookmark(for item: ShelfItem) -> URL? {
guard case .file(let bookmarkData) = item.kind else { return nil }
let bookmark = Bookmark(data: bookmarkData)
let result = bookmark.resolve()
if let refreshed = result.refreshedData, refreshed != bookmarkData {
NSLog("Bookmark for \(item) stale; refreshing")
updateBookmark(for: item, bookmark: refreshed)
}
return result.url
}
func resolveFileURLs(for items: [ShelfItem]) -> [URL] {
var urls: [URL] = []
for it in items {
if let u = resolveFileURL(for: it) { urls.append(u) }
}
return urls
}
}