import Foundation /// The text of the Notitie editor, kept in one plain file. Saves are debounced and atomic. @MainActor public final class NoteStore: ObservableObject { @Published public var text: String = "" { didSet { guard text != oldValue, !loading else { return } changed() } } public private(set) var lastError: String? /// True when the file exists but could not be read: saving would overwrite the user's text, so it is switched off. public private(set) var isReadOnly = false private let fileURL: URL private var saveTask: Task? private var loading = false public init(fileURL: URL) { self.fileURL = fileURL } public func load() { guard FileManager.default.fileExists(atPath: fileURL.path) else { return } loading = true defer { loading = false } do { text = try String(contentsOf: fileURL, encoding: .utf8) } catch { isReadOnly = true lastError = error.localizedDescription } } /// Writes immediately (also call this when the app quits). public func flush() { saveTask?.cancel() saveTask = nil guard !isReadOnly else { return } do { try FileManager.default.createDirectory( at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true) try text.write(to: fileURL, atomically: true, encoding: .utf8) lastError = nil } catch { lastError = error.localizedDescription } } private func changed() { saveTask?.cancel() saveTask = Task { [weak self] in try? await Task.sleep(for: .milliseconds(400)) guard !Task.isCancelled else { return } self?.flush() } } } /// Puts a space between dictations: the text before the cursor decides whether one is needed. public enum DictationSpacing { public static func prepared(_ text: String, characterBefore: Character?) -> String { guard let before = characterBefore, !before.isWhitespace, let first = text.first else { return text } if first.isWhitespace || ".,;:!?)]}\u{201D}\u{2019}".contains(first) { return text } return " " + text } }