| | |
| | | } |
| | | } |
| | | |
| | | for tagName in orderedTagDefinitionNames( |
| | | tagDefinitions: effectiveTagDefinitions, |
| | | tagOrder: tagOrder |
| | | ) { |
| | | let displayName = nameOverrides[tagName] ?? tagName |
| | | guard displayName != macCategory, |
| | | displayName != defaultGroupName |
| | | else { continue } |
| | | |
| | | if containerIDsByGroupName[displayName] == nil { |
| | | containerIDsByGroupName[displayName] = AppContainerID.forTag( |
| | | tagName, |
| | | definition: effectiveTagDefinitions[tagName] |
| | | ) |
| | | } |
| | | if dict[displayName] == nil { |
| | | dict[displayName] = [] |
| | | } |
| | | } |
| | | |
| | | // Build sort index from tagOrder: lower index = appears first |
| | | var orderIndex: [String: Int] = [:] |
| | | for (i, name) in tagOrder.enumerated() { |
| | |
| | | let remaining = apps |
| | | .filter { !orderedPaths.contains($0.path.path) } |
| | | .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } |
| | | return ordered + remaining |
| | | } |
| | | |
| | | private static func orderedTagDefinitionNames( |
| | | tagDefinitions: [String: TagDatabase.TagDef], |
| | | tagOrder: [String] |
| | | ) -> [String] { |
| | | let ordered = tagOrder.filter { tagDefinitions[$0] != nil } |
| | | let orderedSet = Set(ordered) |
| | | let remaining = tagDefinitions.keys |
| | | .filter { !orderedSet.contains($0) } |
| | | .sorted() |
| | | return ordered + remaining |
| | | } |
| | | |
| | |
| | | // 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) |
| | |
| | | guard !FileManager.default.fileExists(atPath: storeURL.path) else { return } |
| | | |
| | | let starterCategoryIDs: [SmartCategoryID] = [ |
| | | .uiPrototyping, |
| | | .ide, |
| | | .writing, |
| | | .game, |
| | | .design, |
| | | .development, |
| | | .office, |
| | | .entertainment, |
| | | .system, |
| | | .systemEnhancement, |
| | | .gtd |
| | | ] |
| | | |
| | |
| | | |
| | | /// 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 |
| | |
| | | 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( |
| | |
| | | previous: previousStore, |
| | | reason: "edit-app-note" |
| | | ) |
| | | return decision |
| | | } |
| | | |
| | | static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) { |