Files
sst/Sources/SstData/NoteStore.swift
T

69 lines
2.3 KiB
Swift

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<Void, Never>?
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
}
}