mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 07:34:32 +00:00
Somewhat working file tray
This commit is contained in:
parent
4c7e79b954
commit
30422c3d95
29 changed files with 4032 additions and 65 deletions
1112
DynamicIsland/components/Shelf/ViewModels/ShelfItemViewModel.swift
Normal file
1112
DynamicIsland/components/Shelf/ViewModels/ShelfItemViewModel.swift
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue