mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
159 lines
5.2 KiB
Swift
159 lines
5.2 KiB
Swift
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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
|
|
}
|
|
}
|