174 lines
5.3 KiB
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
|
|
}
|
|
}
|