import AppKit public struct Note: Identifiable { public let id: UUID public let contents: NSAttributedString public let title: String init(id: UUID, contents: NSAttributedString) { self.id = id self.contents = NSAttributedString(attributedString: contents) var title = "Untitled Note" contents.string.enumerateLines { line, stop in let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { title = String(trimmed.prefix(60)) stop = true } } self.title = title } } @MainActor public final class NotesStore { public private(set) var notes: [Note] = [] private let directory: URL public init(directory: URL = FileManager.default.urls( for: .applicationSupportDirectory, in: .userDomainMask )[0].appendingPathComponent("Conjure/Notes", isDirectory: true)) throws { self.directory = directory try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) let files = try FileManager.default.contentsOfDirectory( at: directory, includingPropertiesForKeys: nil ).filter { $0.pathExtension.lowercased() == "rtfd" } .sorted { $0.lastPathComponent < $1.lastPathComponent } notes = try files.map { url in guard let id = UUID(uuidString: url.deletingPathExtension().lastPathComponent) else { throw CocoaError(.fileReadCorruptFile) } let contents = try NSAttributedString( url: url, options: [.documentType: NSAttributedString.DocumentType.rtfd], documentAttributes: nil ) return Note(id: id, contents: contents) } } public func create() throws -> UUID { let note = Note(id: UUID(), contents: NSAttributedString(string: "")) try save(note) notes.append(note) notes.sort { $0.id.uuidString < $1.id.uuidString } return note.id } public func update(id: UUID, contents: NSAttributedString) throws { guard let index = notes.firstIndex(where: { $0.id == id }) else { throw CocoaError(.fileNoSuchFile) } let note = Note(id: id, contents: contents) try save(note) notes[index] = note } public func delete(id: UUID) throws { guard let index = notes.firstIndex(where: { $0.id == id }) else { throw CocoaError(.fileNoSuchFile) } try FileManager.default.removeItem(at: directory.appendingPathComponent(id.uuidString + ".rtfd")) notes.remove(at: index) } private func save(_ note: Note) throws { let wrapper = try note.contents.fileWrapper( from: NSRange(location: 0, length: note.contents.length), documentAttributes: [.documentType: NSAttributedString.DocumentType.rtfd] ) let url = directory.appendingPathComponent(note.id.uuidString + ".rtfd", isDirectory: true) try wrapper.write(to: url, options: .atomic, originalContentsURL: url) } }