feat: Notitie editor in the app, dictation routed to system or editor
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user