Sign the macOS app and keep its state in SwiftData with iCloud sync
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import SwiftData
|
||||
|
||||
private struct Saved: Codable {
|
||||
struct Composer: Codable {
|
||||
@@ -39,30 +40,43 @@ final class Drafts {
|
||||
private var lostImages: [String: Int] = [:]
|
||||
private var checked: [String: [String]] = [:]
|
||||
private var texts: [String: String] = [:]
|
||||
@ObservationIgnored private let storeKey: String
|
||||
@ObservationIgnored private let scope: String
|
||||
@ObservationIgnored private var timer: Task<Void, Never>?
|
||||
@ObservationIgnored private var dirty = false
|
||||
|
||||
init(origin: URL, login: String) {
|
||||
storeKey = "byre.drafts.\(draftOrigin(origin)).\(login)"
|
||||
let saved = Self.read(storeKey)
|
||||
for (key, d) in saved.composer {
|
||||
if !d.text.isEmpty {
|
||||
composerTexts[key] = d.text
|
||||
}
|
||||
if d.images > 0 {
|
||||
lostImages[key] = d.images
|
||||
scope = "\(draftOrigin(origin)).\(login)"
|
||||
load(Self.read(scope))
|
||||
}
|
||||
|
||||
private func load(_ saved: Saved) {
|
||||
composerTexts = saved.composer.filter { !$0.value.text.isEmpty }.mapValues(\.text)
|
||||
lostImages = saved.composer.reduce(into: [:]) { lost, d in
|
||||
let n = d.value.images - (composerImages[d.key] ?? 0)
|
||||
if n > 0 {
|
||||
lost[d.key] = n
|
||||
}
|
||||
}
|
||||
checked = saved.checked
|
||||
texts = saved.texts
|
||||
}
|
||||
|
||||
private static func read(_ key: String) -> Saved {
|
||||
guard let raw = UserDefaults.standard.string(forKey: key) else {
|
||||
func reload() {
|
||||
if !dirty && sending.isEmpty {
|
||||
load(Self.read(scope))
|
||||
}
|
||||
}
|
||||
|
||||
private static func records(_ scope: String) -> [DraftRecord] {
|
||||
fetchRecords(FetchDescriptor(predicate: #Predicate { $0.scope == scope }, sortBy: [SortDescriptor(\.saved, order: .reverse)]))
|
||||
}
|
||||
|
||||
private static func read(_ scope: String) -> Saved {
|
||||
guard let record = records(scope).first else {
|
||||
return Saved()
|
||||
}
|
||||
do {
|
||||
return try JSONDecoder().decode(Saved.self, from: Data(raw.utf8))
|
||||
return try JSONDecoder().decode(Saved.self, from: record.json)
|
||||
} catch {
|
||||
log.error("read drafts: \(String(describing: error), privacy: .public)")
|
||||
return Saved()
|
||||
@@ -91,6 +105,7 @@ final class Drafts {
|
||||
}
|
||||
|
||||
private func save() {
|
||||
dirty = true
|
||||
timer?.cancel()
|
||||
timer = Task { [weak self] in
|
||||
try? await Task.sleep(for: .milliseconds(400))
|
||||
@@ -103,6 +118,10 @@ final class Drafts {
|
||||
func flush() {
|
||||
timer?.cancel()
|
||||
timer = nil
|
||||
guard dirty else {
|
||||
return
|
||||
}
|
||||
dirty = false
|
||||
var out = Saved(checked: checked, texts: texts)
|
||||
let keys = Set(composerTexts.keys).union(composerImages.keys).union(sending.map(\.key)).union(lostImages.keys)
|
||||
for key in keys {
|
||||
@@ -114,7 +133,18 @@ final class Drafts {
|
||||
}
|
||||
}
|
||||
do {
|
||||
UserDefaults.standard.set(String(decoding: try JSONEncoder().encode(out), as: UTF8.self), forKey: storeKey)
|
||||
let json = try JSONEncoder().encode(out)
|
||||
let records = Self.records(scope)
|
||||
if records.isEmpty {
|
||||
persistence.insert(DraftRecord(scope: scope, json: json))
|
||||
} else {
|
||||
records[0].json = json
|
||||
records[0].saved = Date()
|
||||
for record in records.dropFirst() {
|
||||
persistence.delete(record)
|
||||
}
|
||||
}
|
||||
saveRecords()
|
||||
} catch {
|
||||
log.error("save drafts: \(String(describing: error), privacy: .public)")
|
||||
}
|
||||
@@ -153,6 +183,7 @@ final class Drafts {
|
||||
let send = DraftSend(key: key, text: text, images: images)
|
||||
sending.append(send)
|
||||
composerTexts[key] = nil
|
||||
dirty = true
|
||||
return send
|
||||
}
|
||||
|
||||
@@ -166,6 +197,7 @@ final class Drafts {
|
||||
composerTexts[send.key] = text
|
||||
}
|
||||
}
|
||||
dirty = true
|
||||
flush()
|
||||
}
|
||||
|
||||
@@ -199,6 +231,7 @@ final class Drafts {
|
||||
func clearAnswer(_ id: String) {
|
||||
checked[id] = nil
|
||||
texts[id] = nil
|
||||
dirty = true
|
||||
flush()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user