WIP: add empty color-coded collections
This commit is contained in:
@@ -437,7 +437,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
case .pollProfile:
|
||||
monitor.setPaused(settings.pauseCapture)
|
||||
case .status, .other:
|
||||
case .status, .collections, .other:
|
||||
break
|
||||
case .captureStatus:
|
||||
break
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
final class SettingsModel {
|
||||
enum Change {
|
||||
enum Change: Equatable {
|
||||
case maxHistoryItems
|
||||
case imageCacheMaxBytes
|
||||
case openShortcut
|
||||
@@ -12,6 +12,7 @@ final class SettingsModel {
|
||||
case pauseCapture
|
||||
case pollProfile
|
||||
case captureStatus
|
||||
case collections
|
||||
case status
|
||||
case other
|
||||
}
|
||||
@@ -35,6 +36,8 @@ final class SettingsModel {
|
||||
static let pauseCapture = "pauseCapture"
|
||||
static let clearHistoryOnQuit = "clearHistoryOnQuit"
|
||||
static let accessibilityNoticeShown = "accessibilityNoticeShown"
|
||||
static let customCollectionNames = "customCollectionNames"
|
||||
static let collectionColorHexes = "collectionColorHexes"
|
||||
}
|
||||
|
||||
var maxHistoryItems: Int {
|
||||
@@ -88,6 +91,8 @@ final class SettingsModel {
|
||||
var clearHistoryOnQuit: Bool {
|
||||
didSet { if oldValue != clearHistoryOnQuit { storeAndNotify(.other) } }
|
||||
}
|
||||
private(set) var customCollectionNames: [String]
|
||||
private(set) var collectionColorHexes: [String: String]
|
||||
private(set) var launchAtLoginErrorMessage: String = ""
|
||||
private(set) var accessibilityPermissionStatusMessage: String = ""
|
||||
private(set) var captureStatusMessage: String = ""
|
||||
@@ -123,6 +128,8 @@ final class SettingsModel {
|
||||
excludeSensitive = defaults.object(forKey: Keys.excludeSensitive) as? Bool ?? false
|
||||
pauseCapture = defaults.object(forKey: Keys.pauseCapture) as? Bool ?? false
|
||||
clearHistoryOnQuit = defaults.object(forKey: Keys.clearHistoryOnQuit) as? Bool ?? false
|
||||
customCollectionNames = Self.normalizedCollectionNames(defaults.stringArray(forKey: Keys.customCollectionNames) ?? [])
|
||||
collectionColorHexes = Self.normalizedCollectionColorHexes(defaults.dictionary(forKey: Keys.collectionColorHexes))
|
||||
accessibilityNoticeShown = defaults.object(forKey: Keys.accessibilityNoticeShown) as? Bool ?? false
|
||||
|
||||
maxHistoryItems = max(AppConfiguration.minHistoryLength, min(AppConfiguration.maxHistoryLength, maxHistoryItems))
|
||||
@@ -151,6 +158,8 @@ final class SettingsModel {
|
||||
defaults.set(excludeSensitive, forKey: Keys.excludeSensitive)
|
||||
defaults.set(pauseCapture, forKey: Keys.pauseCapture)
|
||||
defaults.set(clearHistoryOnQuit, forKey: Keys.clearHistoryOnQuit)
|
||||
defaults.set(customCollectionNames, forKey: Keys.customCollectionNames)
|
||||
defaults.set(collectionColorHexes, forKey: Keys.collectionColorHexes)
|
||||
}
|
||||
|
||||
func observe(_ observer: @escaping (Change) -> Void) {
|
||||
@@ -204,6 +213,45 @@ final class SettingsModel {
|
||||
notify(.status)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func ensureCollection(named name: String, colorHex: String? = nil) -> String? {
|
||||
guard let normalizedName = ClipboardCollectionDefaults.normalizedName(name) else { return nil }
|
||||
let existingName = customCollectionNames.first {
|
||||
$0.caseInsensitiveCompare(normalizedName) == .orderedSame
|
||||
}
|
||||
let canonicalName = existingName ?? normalizedName
|
||||
var changed = false
|
||||
|
||||
if existingName == nil {
|
||||
customCollectionNames.append(normalizedName)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if let normalizedHex = Self.normalizedHexColor(colorHex),
|
||||
collectionColorHexes[canonicalName] != normalizedHex {
|
||||
for key in collectionColorHexes.keys where key.caseInsensitiveCompare(canonicalName) == .orderedSame && key != canonicalName {
|
||||
collectionColorHexes.removeValue(forKey: key)
|
||||
}
|
||||
collectionColorHexes[canonicalName] = normalizedHex
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
storeAndNotify(.collections)
|
||||
}
|
||||
return normalizedName
|
||||
}
|
||||
|
||||
func collectionColorHex(forCollectionNamed name: String) -> String? {
|
||||
guard let normalizedName = ClipboardCollectionDefaults.normalizedName(name) else { return nil }
|
||||
if let exact = collectionColorHexes[normalizedName] {
|
||||
return exact
|
||||
}
|
||||
return collectionColorHexes.first { storedName, _ in
|
||||
storedName.caseInsensitiveCompare(normalizedName) == .orderedSame
|
||||
}?.value
|
||||
}
|
||||
|
||||
private static func readShortcut(from value: String?) -> ShortcutBinding? {
|
||||
guard let value else { return nil }
|
||||
return ShortcutBinding(encoded: value)
|
||||
@@ -218,4 +266,39 @@ final class SettingsModel {
|
||||
maxHistoryItems = max(AppConfiguration.minHistoryLength, min(AppConfiguration.maxHistoryLength, maxHistoryItems))
|
||||
imageCacheMaxBytes = max(4 * 1024 * 1024, imageCacheMaxBytes)
|
||||
}
|
||||
|
||||
private static func normalizedCollectionNames(_ names: [String]) -> [String] {
|
||||
var normalized: [String] = []
|
||||
for name in names {
|
||||
guard let value = ClipboardCollectionDefaults.normalizedName(name) else { continue }
|
||||
guard !normalized.contains(where: { $0.caseInsensitiveCompare(value) == .orderedSame }) else { continue }
|
||||
normalized.append(value)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
private static func normalizedCollectionColorHexes(_ rawValue: [String: Any]?) -> [String: String] {
|
||||
guard let rawValue else { return [:] }
|
||||
var normalized: [String: String] = [:]
|
||||
for (name, color) in rawValue {
|
||||
guard let normalizedName = ClipboardCollectionDefaults.normalizedName(name),
|
||||
let hex = normalizedHexColor(color as? String) else {
|
||||
continue
|
||||
}
|
||||
normalized[normalizedName] = hex
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
private static func normalizedHexColor(_ value: String?) -> String? {
|
||||
guard let value else { return nil }
|
||||
var hex = value.clipboardTrimmed.uppercased()
|
||||
if hex.hasPrefix("#") {
|
||||
hex.removeFirst()
|
||||
}
|
||||
guard hex.count == 6, hex.allSatisfy({ $0.isHexDigit }) else {
|
||||
return nil
|
||||
}
|
||||
return "#\(hex)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ struct ClipboardPanelReflowPlan {
|
||||
enum ClipboardPanelShortcutAction: Equatable {
|
||||
case copy
|
||||
case copyPlainText
|
||||
case newCollection
|
||||
case open
|
||||
case pastePlainText
|
||||
case pasteStackNext
|
||||
@@ -398,6 +399,8 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
|
||||
viewModel.copySelected()
|
||||
case .copyPlainText:
|
||||
viewModel.copySelectedPlainText()
|
||||
case .newCollection:
|
||||
panelView.createCollection()
|
||||
case .open:
|
||||
viewModel.openSelected()
|
||||
case .pastePlainText:
|
||||
@@ -493,6 +496,8 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
|
||||
return .toggleStack
|
||||
case 8:
|
||||
return .copyPlainText
|
||||
case 45:
|
||||
return .newCollection
|
||||
case 9:
|
||||
return .pastePlainText
|
||||
case 36:
|
||||
|
||||
@@ -65,7 +65,14 @@ private enum ClipboardCollectionVisuals {
|
||||
}
|
||||
}
|
||||
|
||||
static func color(forCollectionNamed name: String) -> NSColor {
|
||||
static func color(forCollectionNamed name: String, overrideHex: String? = nil) -> NSColor {
|
||||
if let color = color(fromHex: overrideHex) {
|
||||
return color
|
||||
}
|
||||
return defaultColor(forCollectionNamed: name)
|
||||
}
|
||||
|
||||
static func defaultColor(forCollectionNamed name: String) -> NSColor {
|
||||
switch name {
|
||||
case "Useful Links":
|
||||
return customPalette[0]
|
||||
@@ -80,6 +87,34 @@ private enum ClipboardCollectionVisuals {
|
||||
}
|
||||
}
|
||||
|
||||
static func defaultColorHex(forCollectionNamed name: String) -> String {
|
||||
hexString(for: defaultColor(forCollectionNamed: name))
|
||||
}
|
||||
|
||||
static func hexString(for color: NSColor) -> String {
|
||||
let rgb = color.usingColorSpace(.deviceRGB) ?? color
|
||||
let red = Int((rgb.redComponent * 255).rounded())
|
||||
let green = Int((rgb.greenComponent * 255).rounded())
|
||||
let blue = Int((rgb.blueComponent * 255).rounded())
|
||||
return String(format: "#%02X%02X%02X", red, green, blue)
|
||||
}
|
||||
|
||||
private static func color(fromHex value: String?) -> NSColor? {
|
||||
guard let value else { return nil }
|
||||
var hex = value.clipboardTrimmed
|
||||
if hex.hasPrefix("#") {
|
||||
hex.removeFirst()
|
||||
}
|
||||
guard hex.count == 6,
|
||||
let rawValue = Int(hex, radix: 16) else {
|
||||
return nil
|
||||
}
|
||||
let red = CGFloat((rawValue >> 16) & 0xFF) / 255
|
||||
let green = CGFloat((rawValue >> 8) & 0xFF) / 255
|
||||
let blue = CGFloat(rawValue & 0xFF) / 255
|
||||
return NSColor(deviceRed: red, green: green, blue: blue, alpha: 1)
|
||||
}
|
||||
|
||||
private static func stablePaletteIndex(for name: String) -> Int {
|
||||
var hash: UInt64 = 1_469_598_103_934_665_603
|
||||
for scalar in name.lowercased().unicodeScalars {
|
||||
@@ -90,6 +125,11 @@ private enum ClipboardCollectionVisuals {
|
||||
}
|
||||
}
|
||||
|
||||
private struct CollectionCreationRequest {
|
||||
let name: String
|
||||
let colorHex: String
|
||||
}
|
||||
|
||||
final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
private enum Metrics {
|
||||
static let actionButtonSize: CGFloat = 30
|
||||
@@ -549,10 +589,10 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
addCollectionButton.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.16).cgColor
|
||||
addCollectionButton.layer?.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.26).cgColor
|
||||
addCollectionButton.contentTintColor = .secondaryLabelColor
|
||||
addCollectionButton.toolTip = "Add selected clip to a new collection"
|
||||
addCollectionButton.setAccessibilityLabel("Add selected clip to a new collection")
|
||||
addCollectionButton.toolTip = "New collection"
|
||||
addCollectionButton.setAccessibilityLabel("New collection")
|
||||
addCollectionButton.target = self
|
||||
addCollectionButton.action = #selector(addSelectedClipToCollection)
|
||||
addCollectionButton.action = #selector(createCollectionFromToolbar)
|
||||
addCollectionButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
addCollectionButton.widthAnchor.constraint(equalToConstant: 30).isActive = true
|
||||
addCollectionButton.heightAnchor.constraint(equalToConstant: 26).isActive = true
|
||||
@@ -576,7 +616,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
}
|
||||
|
||||
private func collectionColor(forCollectionNamed name: String) -> NSColor {
|
||||
ClipboardCollectionVisuals.color(forCollectionNamed: name)
|
||||
ClipboardCollectionVisuals.color(forCollectionNamed: name, overrideHex: viewModel.collectionColorHex(named: name))
|
||||
}
|
||||
|
||||
private func applyCardDensity() {
|
||||
@@ -670,7 +710,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
isStacked: viewModel.isItemStacked(at: index),
|
||||
stackCount: viewModel.stackCount,
|
||||
canShowInClipboard: viewModel.canShowVisibleItemsInClipboard,
|
||||
selectedCollectionName: viewModel.selectedCollectionName
|
||||
selectedCollectionName: viewModel.selectedCollectionName,
|
||||
selectedCollectionColor: viewModel.selectedCollectionName.map { collectionColor(forCollectionNamed: $0) }
|
||||
)
|
||||
card.onSelect = { [weak self] selected in
|
||||
self?.viewModel.selectItem(at: selected)
|
||||
@@ -812,9 +853,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
}
|
||||
|
||||
private func updateAddCollectionButtonState() {
|
||||
let hasSelectedItem = viewModel.selectedItem != nil
|
||||
addCollectionButton.isEnabled = hasSelectedItem
|
||||
addCollectionButton.alphaValue = hasSelectedItem ? 1.0 : 0.42
|
||||
addCollectionButton.isEnabled = true
|
||||
addCollectionButton.alphaValue = 1.0
|
||||
}
|
||||
|
||||
private func scrollCardIntoView(_ card: NSView) {
|
||||
@@ -856,7 +896,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
if lower.hasPrefix("captured") || lower.contains("capture running") || lower.contains("capture is running") || lower.contains("capture resumed") {
|
||||
return .ready
|
||||
}
|
||||
if lower.hasPrefix("copied") || lower.hasPrefix("pasted") || lower.hasPrefix("updated") || lower.hasPrefix("added") || lower.hasPrefix("removed") || lower.hasPrefix("cleared") || lower.hasPrefix("ignored") || lower.hasPrefix("showing") {
|
||||
if lower.hasPrefix("copied") || lower.hasPrefix("pasted") || lower.hasPrefix("updated") || lower.hasPrefix("added") || lower.hasPrefix("created") || lower.hasPrefix("removed") || lower.hasPrefix("cleared") || lower.hasPrefix("ignored") || lower.hasPrefix("showing") {
|
||||
return .action
|
||||
}
|
||||
if lower.hasPrefix("error") || lower.contains("failed") {
|
||||
@@ -968,6 +1008,13 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
)
|
||||
}
|
||||
|
||||
if let collectionName = viewModel.selectedCollectionName {
|
||||
return (
|
||||
"No clips in \(collectionName)",
|
||||
"Drag clips here or use Collect to add them."
|
||||
)
|
||||
}
|
||||
|
||||
if viewModel.totalItemCount == 0 {
|
||||
return (
|
||||
"Copy something to start your history",
|
||||
@@ -1229,6 +1276,9 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
if stackChip.isSelected {
|
||||
return stackChip.titleText
|
||||
}
|
||||
if let custom = customCollectionButtons.first(where: { $0.value.isSelected }) {
|
||||
return custom.value.titleText
|
||||
}
|
||||
return collectionButtons.first(where: { $0.value.isSelected })?.value.titleText
|
||||
}
|
||||
|
||||
@@ -1248,7 +1298,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
|
||||
var debugCustomCollectionColorHexes: [String: String] {
|
||||
Dictionary(uniqueKeysWithValues: viewModel.collectionNames.map { name in
|
||||
(name, ClipboardItemCardView.debugHex(ClipboardCollectionVisuals.color(forCollectionNamed: name)))
|
||||
(name, ClipboardCollectionVisuals.hexString(for: collectionColor(forCollectionNamed: name)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1294,7 +1344,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
}
|
||||
|
||||
func debugPressAddCollectionButton() {
|
||||
addSelectedClipToCollection()
|
||||
createCollectionFromToolbar()
|
||||
}
|
||||
|
||||
func debugShowFirstCardInClipboard() {
|
||||
@@ -1373,18 +1423,25 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
searchField.stringValue = viewModel.searchText
|
||||
}
|
||||
|
||||
@objc private func addSelectedClipToCollection() {
|
||||
guard viewModel.selectedItem != nil,
|
||||
let name = requestCollectionName() else {
|
||||
return
|
||||
}
|
||||
viewModel.assignSelected(to: name)
|
||||
func createCollection() {
|
||||
createCollectionFromToolbar()
|
||||
}
|
||||
|
||||
private func requestCollectionName() -> String? {
|
||||
@objc private func createCollectionFromToolbar() {
|
||||
guard let request = requestCollectionCreation() else { return }
|
||||
viewModel.createCollection(named: request.name, colorHex: request.colorHex, selectAfterCreate: true)
|
||||
}
|
||||
|
||||
private func requestCollectionCreation() -> CollectionCreationRequest? {
|
||||
#if DEBUG
|
||||
if let collectionNameProviderForTesting {
|
||||
return ClipboardCollectionDefaults.normalizedName(collectionNameProviderForTesting())
|
||||
guard let name = ClipboardCollectionDefaults.normalizedName(collectionNameProviderForTesting()) else {
|
||||
return nil
|
||||
}
|
||||
return CollectionCreationRequest(
|
||||
name: name,
|
||||
colorHex: ClipboardCollectionVisuals.defaultColorHex(forCollectionNamed: name)
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1392,18 +1449,40 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
input.placeholderString = "Collection name"
|
||||
input.stringValue = ""
|
||||
|
||||
let colorWell = NSColorWell(frame: NSRect(x: 0, y: 0, width: 48, height: 28))
|
||||
colorWell.color = ClipboardCollectionVisuals.defaultColor(forCollectionNamed: "New Collection")
|
||||
|
||||
let colorLabel = NSTextField(labelWithString: "Color")
|
||||
colorLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .medium)
|
||||
colorLabel.textColor = .secondaryLabelColor
|
||||
|
||||
let colorRow = NSStackView(views: [colorLabel, colorWell])
|
||||
colorRow.orientation = .horizontal
|
||||
colorRow.alignment = .centerY
|
||||
colorRow.spacing = 10
|
||||
|
||||
let stack = NSStackView(views: [input, colorRow])
|
||||
stack.orientation = .vertical
|
||||
stack.alignment = .leading
|
||||
stack.spacing = 10
|
||||
stack.frame = NSRect(x: 0, y: 0, width: 260, height: 64)
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "New Collection"
|
||||
alert.informativeText = "Name this collection and add the selected clip to it."
|
||||
alert.accessoryView = input
|
||||
alert.addButton(withTitle: "Add")
|
||||
alert.informativeText = "Name this collection and choose its color."
|
||||
alert.accessoryView = stack
|
||||
alert.addButton(withTitle: "Create")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.window.initialFirstResponder = input
|
||||
|
||||
guard alert.runModal() == .alertFirstButtonReturn else {
|
||||
guard alert.runModal() == .alertFirstButtonReturn,
|
||||
let name = ClipboardCollectionDefaults.normalizedName(input.stringValue) else {
|
||||
return nil
|
||||
}
|
||||
return ClipboardCollectionDefaults.normalizedName(input.stringValue)
|
||||
return CollectionCreationRequest(
|
||||
name: name,
|
||||
colorHex: ClipboardCollectionVisuals.hexString(for: colorWell.color)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1720,7 +1799,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
isStacked: Bool = false,
|
||||
stackCount: Int = 0,
|
||||
canShowInClipboard: Bool = false,
|
||||
selectedCollectionName: String? = nil
|
||||
selectedCollectionName: String? = nil,
|
||||
selectedCollectionColor: NSColor? = nil
|
||||
) {
|
||||
let normalizedItemCollection = ClipboardCollectionDefaults.normalizedName(item.collectionName)
|
||||
let normalizedSelectedCollection = ClipboardCollectionDefaults.normalizedName(selectedCollectionName)
|
||||
@@ -1737,7 +1817,9 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
self.itemSourceAppBundleID = Self.presentSourceText(item.sourceAppBundleId)
|
||||
self.itemCollectionName = normalizedItemCollection
|
||||
self.activeCollectionName = activeCollection
|
||||
self.activeCollectionColor = activeCollection.map(ClipboardCollectionVisuals.color(forCollectionNamed:))
|
||||
self.activeCollectionColor = activeCollection.map { name in
|
||||
selectedCollectionColor ?? ClipboardCollectionVisuals.color(forCollectionNamed: name)
|
||||
}
|
||||
self.collectionNames = collectionNames.compactMap { ClipboardCollectionDefaults.normalizedName($0) }
|
||||
super.init(frame: .zero)
|
||||
configure(item: item, thumbnail: thumbnail)
|
||||
@@ -1925,11 +2007,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
}
|
||||
|
||||
static func debugHex(_ color: NSColor) -> String {
|
||||
let rgb = color.usingColorSpace(.deviceRGB) ?? color
|
||||
let red = Int((rgb.redComponent * 255).rounded())
|
||||
let green = Int((rgb.greenComponent * 255).rounded())
|
||||
let blue = Int((rgb.blueComponent * 255).rounded())
|
||||
return String(format: "#%02X%02X%02X", red, green, blue)
|
||||
ClipboardCollectionVisuals.hexString(for: color)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -111,11 +111,18 @@ final class ClipboardPanelViewModel {
|
||||
}
|
||||
}
|
||||
settings.observe { [weak self] change in
|
||||
guard case .captureStatus = change else { return }
|
||||
self?.notifyMain {
|
||||
self?.statusMessage = ""
|
||||
self?.onStatusMessageChanged?("")
|
||||
self?.onCaptureStatusChanged?()
|
||||
switch change {
|
||||
case .captureStatus:
|
||||
self?.statusMessage = ""
|
||||
self?.onStatusMessageChanged?("")
|
||||
self?.onCaptureStatusChanged?()
|
||||
case .collections:
|
||||
self?.recomputeVisibleItems()
|
||||
self?.onCollectionsChanged?()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,11 +162,20 @@ final class ClipboardPanelViewModel {
|
||||
ClipboardCollectionDefaults.normalizedName(item.collectionName)
|
||||
}
|
||||
)
|
||||
let defaultNames = ClipboardCollectionDefaults.names.filter { assignedNames.contains($0) }
|
||||
let customNames = assignedNames
|
||||
let configuredNames = settings.customCollectionNames
|
||||
let configuredNameSet = Set(configuredNames.map { $0.lowercased() })
|
||||
let allNames = assignedNames.union(configuredNames)
|
||||
let defaultNames = ClipboardCollectionDefaults.names.filter { allNames.contains($0) }
|
||||
var configuredCustomNames: [String] = []
|
||||
for name in configuredNames where !ClipboardCollectionDefaults.names.contains(name) {
|
||||
guard !configuredCustomNames.contains(where: { $0.caseInsensitiveCompare(name) == .orderedSame }) else { continue }
|
||||
configuredCustomNames.append(name)
|
||||
}
|
||||
let assignedCustomNames = assignedNames
|
||||
.filter { !ClipboardCollectionDefaults.names.contains($0) }
|
||||
.filter { !configuredNameSet.contains($0.lowercased()) }
|
||||
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
|
||||
return defaultNames + customNames
|
||||
return defaultNames + configuredCustomNames + assignedCustomNames
|
||||
}
|
||||
|
||||
func collectionCount(for sortMode: ClipboardSortMode) -> Int {
|
||||
@@ -472,6 +488,21 @@ final class ClipboardPanelViewModel {
|
||||
selectedCollectionName = normalizedName
|
||||
}
|
||||
|
||||
func createCollection(named name: String, colorHex: String? = nil, selectAfterCreate: Bool = true) {
|
||||
guard let normalizedName = settings.ensureCollection(named: name, colorHex: colorHex) else { return }
|
||||
statusMessage = "Created \(normalizedName)"
|
||||
if selectAfterCreate {
|
||||
selectCollection(named: normalizedName)
|
||||
} else {
|
||||
recomputeVisibleItems()
|
||||
onCollectionsChanged?()
|
||||
}
|
||||
}
|
||||
|
||||
func collectionColorHex(named name: String) -> String? {
|
||||
settings.collectionColorHex(forCollectionNamed: name)
|
||||
}
|
||||
|
||||
func clearSearch() {
|
||||
searchText = ""
|
||||
}
|
||||
@@ -857,6 +888,9 @@ final class ClipboardPanelViewModel {
|
||||
|
||||
private func assign(item: ClipboardItem, to collectionName: String?) {
|
||||
let normalizedName = ClipboardCollectionDefaults.normalizedName(collectionName)
|
||||
if let normalizedName {
|
||||
settings.ensureCollection(named: normalizedName)
|
||||
}
|
||||
store.setCollection(item.id, name: normalizedName)
|
||||
if let normalizedName {
|
||||
statusMessage = "Added to \(normalizedName)"
|
||||
|
||||
Reference in New Issue
Block a user