/* * 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 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 = [] // 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 } func reconcileSelection(with allItems: [ShelfItem]) { let validIDs = Set(allItems.map(\.id)) let filteredSelection = selectedIDs.intersection(validIDs) if filteredSelection != selectedIDs { selectedIDs = filteredSelection } if let anchor = lastAnchorID, !validIDs.contains(anchor) { lastAnchorID = selectedIDs.first } } // 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 } }