Ariver
2026-06-30 4c6f31c43a03ecb5a85ffb6a3ea3905a661af0f4
src/Apptag/DataLayer.swift
@@ -237,6 +237,52 @@
// MARK: - Tag Color Mapping
struct TagCustomColor: Codable, Equatable {
    var red: Double
    var green: Double
    var blue: Double
    var alpha: Double
    init(red: Double, green: Double, blue: Double, alpha: Double = 1.0) {
        self.red = Self.clamp(red)
        self.green = Self.clamp(green)
        self.blue = Self.clamp(blue)
        self.alpha = Self.clamp(alpha)
    }
    init(nsColor: NSColor) {
        let rgbColor = nsColor.usingColorSpace(.sRGB) ?? nsColor
        self.init(
            red: Double(rgbColor.redComponent),
            green: Double(rgbColor.greenComponent),
            blue: Double(rgbColor.blueComponent),
            alpha: Double(rgbColor.alphaComponent)
        )
    }
    var nsColor: NSColor {
        NSColor(
            srgbRed: CGFloat(red),
            green: CGFloat(green),
            blue: CGFloat(blue),
            alpha: CGFloat(alpha)
        )
    }
    var signature: String {
        [
            String(format: "%.4f", red),
            String(format: "%.4f", green),
            String(format: "%.4f", blue),
            String(format: "%.4f", alpha)
        ].joined(separator: ":")
    }
    private static func clamp(_ value: Double) -> Double {
        min(1.0, max(0.0, value))
    }
}
enum TagColor {
    static func nsColor(for index: Int) -> NSColor {
        switch index {
@@ -251,6 +297,20 @@
        default: return NSColor.systemGray
        }
    }
    static func nsColor(for index: Int, customColor: TagCustomColor?) -> NSColor {
        customColor?.nsColor ?? nsColor(for: index)
    }
    static func prefersDarkText(for color: NSColor) -> Bool {
        let rgbColor = color.usingColorSpace(.sRGB) ?? color
        let red = Double(rgbColor.redComponent)
        let green = Double(rgbColor.greenComponent)
        let blue = Double(rgbColor.blueComponent)
        let luminance = (0.299 * red) + (0.587 * green) + (0.114 * blue)
        return luminance > 0.62
    }
    static let allIndices: [Int] = [1, 2, 3, 4, 5, 6, 7]
}
@@ -963,6 +1023,7 @@
    struct TagDef: Codable, Equatable {
        var color: Int
        var systemCategoryID: SmartCategoryID? = nil
        var customColor: TagCustomColor? = nil
    }
    struct Store: Codable {
@@ -1551,12 +1612,14 @@
    // MARK: Export / Import
    static func exportTo(_ url: URL) throws {
        try ProEntitlementPolicy.requireUnlocked(.layoutExport)
        let store = loadWithEnsuredCategoryScheme()
        let data = try JSONEncoder().encode(store)
        try data.write(to: url, options: .atomic)
    }
    static func importFrom(_ url: URL) throws -> Store {
        try ProEntitlementPolicy.requireUnlocked(.layoutImport)
        let previousStore = loadWithEnsuredCategoryScheme()
        let data = try Data(contentsOf: url)
        var store = try JSONDecoder().decode(Store.self, from: data)
@@ -1753,12 +1816,11 @@
        guard !FileManager.default.fileExists(atPath: storeURL.path) else { return }
        let starterCategoryIDs: [SmartCategoryID] = [
            .uiPrototyping,
            .ide,
            .writing,
            .game,
            .design,
            .development,
            .office,
            .entertainment,
            .system,
            .systemEnhancement,
            .gtd
        ]
@@ -1808,6 +1870,8 @@
    /// Persist the visible app order for one stable container.
    static func reorderApps(inContainer containerID: String, orderedPaths: [String]) {
        guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return }
        var store = TagDatabase.load()
        _ = TagDatabase.normalizeContainerAppOrder(in: &store)
        let previousStore = store
@@ -2137,11 +2201,22 @@
        TagDatabase.save(store)
    }
    static func setAppNote(_ note: String, for path: String) {
    @discardableResult
    static func setAppNote(_ note: String, for path: String) -> ProNoteSaveDecision {
        var store = TagDatabase.load()
        let previousStore = store
        let trimmed = note.trimmingCharacters(in: .whitespacesAndNewlines)
        let limited = String(trimmed.prefix(TagDatabase.maxAppNoteLength))
        let decision = ProEntitlementPolicy.noteSaveDecision(
            note: limited,
            for: path,
            in: store
        )
        guard case .allow = decision else {
            return decision
        }
        let previousStore = store
        if limited.isEmpty {
            store.appNotes.removeValue(forKey: path)
            store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata(
@@ -2167,6 +2242,7 @@
            previous: previousStore,
            reason: "edit-app-note"
        )
        return decision
    }
    static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) {
@@ -2272,6 +2348,7 @@
        let previousStore = store
        var tagDef = store.tags[tag] ?? TagDatabase.TagDef(color: color, systemCategoryID: nil)
        tagDef.color = color
        tagDef.customColor = nil
        store.tags[tag] = tagDef
        TagDatabase.saveUserCategorySchemeMutation(
            store,
@@ -2279,4 +2356,23 @@
            reason: "set-tag-color"
        )
    }
    /// Set a Pro custom color for a tag without changing its fallback base color.
    @discardableResult
    static func setCustomColor(_ customColor: TagCustomColor, for tag: String) -> Bool {
        guard ProEntitlementPolicy.isUnlocked(.customTagColors) else {
            return false
        }
        var store = TagDatabase.load()
        let previousStore = store
        var tagDef = store.tags[tag] ?? TagDatabase.TagDef(color: 0, systemCategoryID: nil)
        tagDef.customColor = customColor
        store.tags[tag] = tagDef
        TagDatabase.saveUserCategorySchemeMutation(
            store,
            previous: previousStore,
            reason: "set-tag-custom-color"
        )
        return true
    }
}