/* * Atoll (DynamicIsland) * Copyright (C) 2024-2026 Atoll Contributors * * Originally from boring.notch project * Modified and adapted for Atoll (DynamicIsland) * See NOTICE for details. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Foundation import AppKit @MainActor final class ShelfStateViewModel: ObservableObject { static let shared = ShelfStateViewModel() @Published private(set) var items: [ShelfItem] = [] { didSet { ShelfPersistenceService.shared.save(items) ShelfSelectionModel.shared.reconcileSelection(with: 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? 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 = Set(merged.map { $0.identityKey }) var addedIDs: [String] = [] for it in newItems { let key = it.identityKey if !seen.contains(key) { merged.append(it) seen.insert(key) addedIDs.append(it.id.uuidString) } } items = merged if !addedIDs.isEmpty { ExtensionRPCServer.shared.notifyShelfItemsChanged(itemIDs: addedIDs, action: "added") } } func remove(_ item: ShelfItem) { item.cleanupStoredData() items.removeAll { $0.id == item.id } ExtensionRPCServer.shared.notifyShelfItemsChanged(itemIDs: [item.id.uuidString], action: "removed") } 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 } }