Tusker/Packages/ComposeUI/Sources/ComposeUI/Model/DraftsManager.swift

174 lines
5.3 KiB
Swift

//
// DraftsManager.swift
// ComposeUI
//
// Created by Shadowfacts on 10/22/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
import Combine
import OSLog
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DraftsManager")
private let encoder = PropertyListEncoder()
private let decoder = PropertyListDecoder()
public class DraftsManager: NSObject, ObservableObject, NSFilePresenter {
public private(set) static var shared: DraftsManager = {
let draftsManager = DraftsManager(url: DraftsManager.archiveURL)
draftsManager.load()
return draftsManager
}()
private static var appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
private static var archiveURL = appGroupDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
private var url: URL
@Published private var drafts: [UUID: Draft] = [:]
public init(url: URL) {
self.url = url
super.init()
}
private func merge(from container: DraftsContainer) {
for draft in container.drafts.values {
if let existing = self.drafts[draft.id] {
existing.merge(from: draft)
} else {
self.drafts[draft.id] = draft
}
}
for id in self.drafts.keys where !container.drafts.keys.contains(id) {
self.drafts.removeValue(forKey: id)
}
}
public func load(completion: ((Error?) -> Void)? = nil) {
NSFileCoordinator(filePresenter: self).coordinate(readingItemAt: self.url, options: [], error: nil) { url in
do {
let data = try Data(contentsOf: url)
let container = try decoder.decode(DraftsContainer.self, from: data)
DispatchQueue.main.async {
self.merge(from: container)
completion?(nil)
}
} catch {
logger.error("Error loading: \(String(describing: error))")
completion?(error)
}
}
}
public func migrate(from url: URL) -> Result<Void, any Error> {
do {
let data = try Data(contentsOf: url)
let container = try decoder.decode(DraftsContainer.self, from: data)
self.merge(from: container)
self.save()
} catch {
logger.error("Error migrating: \(String(describing: error))")
return .failure(error)
}
return .success(())
}
public func save(completion: ((Error?) -> Void)? = nil) {
NSFileCoordinator(filePresenter: self).coordinate(writingItemAt: Self.archiveURL, options: .forReplacing, error: nil) { url in
do {
let data = try encoder.encode(DraftsContainer(drafts: self.drafts))
try data.write(to: url, options: .atomic)
completion?(nil)
} catch {
logger.error("Error saving: \(String(describing: error))")
completion?(error)
}
}
}
// MARK: Drafts API
var sorted: [Draft] {
return drafts.values.sorted(by: { $0.lastModified > $1.lastModified })
}
public func add(_ draft: Draft) {
drafts[draft.id] = draft
}
public func remove(_ draft: Draft) {
drafts.removeValue(forKey: draft.id)
}
public func getBy(id: UUID) -> Draft? {
return drafts[id]
}
// MARK: NSFilePresenter
public var presentedItemURL: URL? {
url
}
public let presentedItemOperationQueue = OperationQueue()
public func presentedItemDidMove(to newURL: URL) {
self.url = newURL
}
public func presentedItemDidChange() {
self.load()
}
// MARK: Supporting Types
struct DraftsContainer: Codable {
let drafts: [UUID: Draft]
init(drafts: [UUID: Draft]) {
self.drafts = drafts
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.drafts = try container.decode([UUID: SafeDraft].self, forKey: .drafts).compactMapValues(\.draft)
}
enum CodingKeys: CodingKey {
case drafts
}
}
// a container that always succeeds at decoding
// so if a single draft can't be decoded, we don't lose all drafts
struct SafeDraft: Decodable {
let draft: Draft?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.draft = try? container.decode(Draft.self)
}
}
}
private extension Draft {
func merge(from other: Draft) {
self.lastModified = other.lastModified
self.accountID = other.accountID
self.text = other.text
self.contentWarningEnabled = other.contentWarningEnabled
self.contentWarning = other.contentWarning
self.attachments = other.attachments
self.inReplyToID = other.inReplyToID
self.visibility = other.visibility
self.poll = other.poll
self.localOnly = other.localOnly
self.initialText = other.initialText
}
}