mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 04:41:23 +08:00
feat: open-source 小浣熊阅读 for iOS
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import Foundation
|
||||
|
||||
public struct RSSSourceImportPayload: Sendable {
|
||||
public var sources: [RssSource]
|
||||
public var subscriptionURLs: [String]
|
||||
public init(sources: [RssSource] = [], subscriptionURLs: [String] = []) {
|
||||
self.sources = sources
|
||||
self.subscriptionURLs = subscriptionURLs
|
||||
}
|
||||
}
|
||||
|
||||
public enum RSSSourceImportError: Error, LocalizedError, Sendable {
|
||||
case emptyDocument
|
||||
case wrongFormat
|
||||
case invalidSource
|
||||
case duplicate(String)
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .emptyDocument: "导入内容为空"
|
||||
case .wrongFormat: "RSS源格式错误"
|
||||
case .invalidSource: "不是订阅源"
|
||||
case .duplicate(let url): "RSS源已存在:\(url)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct RSSSourceImporter: Sendable {
|
||||
public init() {}
|
||||
|
||||
public func decode(_ data: Data) throws -> RSSSourceImportPayload {
|
||||
var data = data
|
||||
if data.starts(with: [0xEF, 0xBB, 0xBF]) { data = Data(data.dropFirst(3)) }
|
||||
guard !data.isEmpty else { throw RSSSourceImportError.emptyDocument }
|
||||
let decoder = JSONDecoder()
|
||||
if let manifest = try? decoder.decode(SubscriptionManifest.self, from: data), !manifest.sourceUrls.isEmpty {
|
||||
return .init(subscriptionURLs: manifest.sourceUrls)
|
||||
}
|
||||
let sources: [RssSource]
|
||||
if let values = try? decoder.decode([RssSource].self, from: data) { sources = values }
|
||||
else if let value = try? decoder.decode(RssSource.self, from: data) { sources = [value] }
|
||||
else { throw RSSSourceImportError.wrongFormat }
|
||||
guard sources.isEmpty || sources.first?.sourceUrl.isEmpty == false else { throw RSSSourceImportError.invalidSource }
|
||||
return .init(sources: sources)
|
||||
}
|
||||
|
||||
public func merge(
|
||||
_ incoming: [RssSource],
|
||||
into existing: [RssSource],
|
||||
duplicatePolicy: DuplicatePolicy = .replace,
|
||||
addGroup: String? = nil
|
||||
) throws -> [RssSource] {
|
||||
var result = existing
|
||||
var indexes = Dictionary(uniqueKeysWithValues: result.enumerated().map { ($0.element.sourceUrl, $0.offset) })
|
||||
for original in incoming where !original.sourceUrl.isEmpty {
|
||||
var source = original
|
||||
if let addGroup, !addGroup.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { source.addGroups(addGroup) }
|
||||
if let index = indexes[source.sourceUrl] {
|
||||
switch duplicatePolicy {
|
||||
case .replace: result[index] = source
|
||||
case .skip: continue
|
||||
case .fail: throw RSSSourceImportError.duplicate(source.sourceUrl)
|
||||
}
|
||||
} else {
|
||||
indexes[source.sourceUrl] = result.count
|
||||
result.append(source)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func defaultSelection(incoming: [RssSource], existing: [RssSource]) -> [Bool] {
|
||||
let current = Dictionary(uniqueKeysWithValues: existing.map { ($0.sourceUrl, $0.lastUpdateTime) })
|
||||
return incoming.map { source in
|
||||
guard let oldTime = current[source.sourceUrl] else { return true }
|
||||
return oldTime < source.lastUpdateTime
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(_ sources: [RssSource], prettyPrinted: Bool = true) throws -> Data {
|
||||
let encoder = JSONEncoder()
|
||||
if prettyPrinted { encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] }
|
||||
return try encoder.encode(sources)
|
||||
}
|
||||
|
||||
private struct SubscriptionManifest: Decodable {
|
||||
var sourceUrls: [String]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user