343 lines
10 KiB
Swift
343 lines
10 KiB
Swift
import Foundation
|
|
import UIKit
|
|
|
|
public let kRDEPUBHighlightAttributeName = NSAttributedString.Key("com.RDEpubReader.highlight")
|
|
|
|
public let kRDEPUBUnderlineAttributeName = NSAttributedString.Key("com.RDEpubReader.underline")
|
|
|
|
public struct RDEPUBSelection: Codable, Equatable {
|
|
|
|
public var bookIdentifier: String?
|
|
|
|
public var location: RDEPUBLocation
|
|
|
|
public var text: String
|
|
|
|
public var rangeInfo: String?
|
|
|
|
public var createdAt: Date
|
|
|
|
public init(
|
|
bookIdentifier: String? = nil,
|
|
location: RDEPUBLocation,
|
|
text: String,
|
|
rangeInfo: String? = nil,
|
|
createdAt: Date = Date()
|
|
) {
|
|
self.bookIdentifier = bookIdentifier
|
|
self.location = location
|
|
self.text = text
|
|
self.rangeInfo = rangeInfo?.nilIfEmpty
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
public var isEmpty: Bool {
|
|
text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
}
|
|
}
|
|
|
|
public enum RDEPUBHighlightStyle: String, Codable {
|
|
case highlight
|
|
case underline
|
|
}
|
|
|
|
public enum RDEPUBAnnotationKind: String, Codable, Equatable {
|
|
case bookmark
|
|
case highlight
|
|
case underline
|
|
}
|
|
|
|
public enum RDEPUBAnnotationMenuAction: Equatable {
|
|
case copy
|
|
case highlight
|
|
case annotate
|
|
}
|
|
|
|
public struct RDEPUBTextOffsetRangeInfo: Codable, Equatable {
|
|
|
|
public var kind: String
|
|
|
|
public var href: String
|
|
|
|
public var start: Int
|
|
|
|
public var end: Int
|
|
|
|
public init(href: String, start: Int, end: Int) {
|
|
self.kind = "text-offset"
|
|
self.href = href
|
|
self.start = start
|
|
self.end = end
|
|
}
|
|
|
|
public var nsRange: NSRange? {
|
|
guard end > start else { return nil }
|
|
return NSRange(location: start, length: end - start)
|
|
}
|
|
|
|
public func jsonString() -> String? {
|
|
guard let data = try? JSONEncoder().encode(self) else { return nil }
|
|
return String(data: data, encoding: .utf8)
|
|
}
|
|
|
|
public static func decode(from string: String?) -> RDEPUBTextOffsetRangeInfo? {
|
|
guard let string,
|
|
let data = string.data(using: .utf8),
|
|
let rangeInfo = try? JSONDecoder().decode(RDEPUBTextOffsetRangeInfo.self, from: data),
|
|
rangeInfo.kind == "text-offset",
|
|
rangeInfo.end > rangeInfo.start else {
|
|
return nil
|
|
}
|
|
return rangeInfo
|
|
}
|
|
}
|
|
|
|
public struct RDEPUBHighlight: Codable, Equatable {
|
|
|
|
public var id: String
|
|
|
|
public var bookIdentifier: String?
|
|
|
|
public var location: RDEPUBLocation
|
|
|
|
public var text: String
|
|
|
|
public var rangeInfo: String?
|
|
|
|
public var style: RDEPUBHighlightStyle
|
|
|
|
public var color: String
|
|
|
|
public var note: String?
|
|
|
|
/// True when this record was created directly as an annotation instead of
|
|
/// adding a note to an existing underline.
|
|
public var isAnnotationOnly: Bool
|
|
|
|
public var createdAt: Date
|
|
|
|
public init(
|
|
id: String = UUID().uuidString,
|
|
bookIdentifier: String? = nil,
|
|
location: RDEPUBLocation,
|
|
text: String,
|
|
rangeInfo: String? = nil,
|
|
style: RDEPUBHighlightStyle = .underline,
|
|
color: String = "#F8E16C",
|
|
note: String? = nil,
|
|
isAnnotationOnly: Bool = false,
|
|
createdAt: Date = Date()
|
|
) {
|
|
self.id = id
|
|
self.bookIdentifier = bookIdentifier
|
|
self.location = location
|
|
self.text = text
|
|
self.rangeInfo = rangeInfo?.nilIfEmpty
|
|
self.style = style
|
|
self.color = color
|
|
self.note = note?.nilIfEmpty
|
|
self.isAnnotationOnly = isAnnotationOnly
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
public var hasNote: Bool {
|
|
note?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
|
}
|
|
|
|
public var uiColor: UIColor {
|
|
UIColor(rdHexString: color, alpha: 0.35)
|
|
?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.35)
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case bookIdentifier
|
|
case location
|
|
case text
|
|
case rangeInfo
|
|
case style
|
|
case color
|
|
case note
|
|
case isAnnotationOnly
|
|
case createdAt
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
id = try container.decode(String.self, forKey: .id)
|
|
bookIdentifier = try container.decodeIfPresent(String.self, forKey: .bookIdentifier)
|
|
location = try container.decode(RDEPUBLocation.self, forKey: .location)
|
|
text = try container.decode(String.self, forKey: .text)
|
|
rangeInfo = try container.decodeIfPresent(String.self, forKey: .rangeInfo)?.nilIfEmpty
|
|
if let rawStyle = try container.decodeIfPresent(String.self, forKey: .style),
|
|
let decodedStyle = RDEPUBHighlightStyle(rawValue: rawStyle) {
|
|
style = decodedStyle == .highlight ? .underline : decodedStyle
|
|
} else {
|
|
style = .underline
|
|
}
|
|
color = try container.decodeIfPresent(String.self, forKey: .color) ?? "#F8E16C"
|
|
note = try container.decodeIfPresent(String.self, forKey: .note)?.nilIfEmpty
|
|
isAnnotationOnly = try container.decodeIfPresent(Bool.self, forKey: .isAnnotationOnly) ?? false
|
|
createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? Date()
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(id, forKey: .id)
|
|
try container.encodeIfPresent(bookIdentifier, forKey: .bookIdentifier)
|
|
try container.encode(location, forKey: .location)
|
|
try container.encode(text, forKey: .text)
|
|
try container.encodeIfPresent(rangeInfo, forKey: .rangeInfo)
|
|
try container.encode(style, forKey: .style)
|
|
try container.encode(color, forKey: .color)
|
|
try container.encodeIfPresent(note, forKey: .note)
|
|
try container.encode(isAnnotationOnly, forKey: .isAnnotationOnly)
|
|
try container.encode(createdAt, forKey: .createdAt)
|
|
}
|
|
|
|
public var annotation: RDEPUBAnnotation {
|
|
RDEPUBAnnotation(highlight: self)
|
|
}
|
|
}
|
|
|
|
public struct RDEPUBBookmark: Codable, Equatable {
|
|
|
|
public var id: String
|
|
|
|
public var bookIdentifier: String?
|
|
|
|
public var location: RDEPUBLocation
|
|
|
|
public var rangeInfo: String?
|
|
|
|
public var chapterTitle: String?
|
|
|
|
public var note: String?
|
|
|
|
public var createdAt: Date
|
|
|
|
public init(
|
|
id: String = UUID().uuidString,
|
|
bookIdentifier: String? = nil,
|
|
location: RDEPUBLocation,
|
|
rangeInfo: String? = nil,
|
|
chapterTitle: String? = nil,
|
|
note: String? = nil,
|
|
createdAt: Date = Date()
|
|
) {
|
|
self.id = id
|
|
self.bookIdentifier = bookIdentifier
|
|
self.location = location
|
|
self.rangeInfo = rangeInfo?.nilIfEmpty
|
|
self.chapterTitle = chapterTitle?.nilIfEmpty
|
|
self.note = note?.nilIfEmpty
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
public var hasNote: Bool {
|
|
note?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
|
}
|
|
|
|
public var annotation: RDEPUBAnnotation {
|
|
RDEPUBAnnotation(bookmark: self)
|
|
}
|
|
}
|
|
|
|
public struct RDEPUBAnnotation: Codable, Equatable {
|
|
public var id: String
|
|
public var bookIdentifier: String?
|
|
public var kind: RDEPUBAnnotationKind
|
|
public var location: RDEPUBLocation
|
|
public var text: String?
|
|
public var rangeInfo: String?
|
|
public var color: String?
|
|
public var chapterTitle: String?
|
|
public var note: String?
|
|
public var createdAt: Date
|
|
|
|
public init(
|
|
id: String = UUID().uuidString,
|
|
bookIdentifier: String? = nil,
|
|
kind: RDEPUBAnnotationKind,
|
|
location: RDEPUBLocation,
|
|
text: String? = nil,
|
|
rangeInfo: String? = nil,
|
|
color: String? = nil,
|
|
chapterTitle: String? = nil,
|
|
note: String? = nil,
|
|
createdAt: Date = Date()
|
|
) {
|
|
self.id = id
|
|
self.bookIdentifier = bookIdentifier
|
|
self.kind = kind
|
|
self.location = location
|
|
self.text = text?.nilIfEmpty
|
|
self.rangeInfo = rangeInfo?.nilIfEmpty
|
|
self.color = color?.nilIfEmpty
|
|
self.chapterTitle = chapterTitle?.nilIfEmpty
|
|
self.note = note?.nilIfEmpty
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
public init(highlight: RDEPUBHighlight) {
|
|
self.init(
|
|
id: highlight.id,
|
|
bookIdentifier: highlight.bookIdentifier,
|
|
kind: highlight.style == .underline ? .underline : .highlight,
|
|
location: highlight.location,
|
|
text: highlight.text,
|
|
rangeInfo: highlight.rangeInfo,
|
|
color: highlight.color,
|
|
chapterTitle: nil,
|
|
note: highlight.note,
|
|
createdAt: highlight.createdAt
|
|
)
|
|
}
|
|
|
|
public init(bookmark: RDEPUBBookmark) {
|
|
self.init(
|
|
id: bookmark.id,
|
|
bookIdentifier: bookmark.bookIdentifier,
|
|
kind: .bookmark,
|
|
location: bookmark.location,
|
|
text: nil,
|
|
rangeInfo: bookmark.rangeInfo,
|
|
color: nil,
|
|
chapterTitle: bookmark.chapterTitle,
|
|
note: bookmark.note,
|
|
createdAt: bookmark.createdAt
|
|
)
|
|
}
|
|
|
|
public var bookmark: RDEPUBBookmark? {
|
|
guard kind == .bookmark else { return nil }
|
|
return RDEPUBBookmark(
|
|
id: id,
|
|
bookIdentifier: bookIdentifier,
|
|
location: location,
|
|
rangeInfo: rangeInfo,
|
|
chapterTitle: chapterTitle,
|
|
note: note,
|
|
createdAt: createdAt
|
|
)
|
|
}
|
|
|
|
public var highlight: RDEPUBHighlight? {
|
|
guard kind == .highlight || kind == .underline,
|
|
let text else {
|
|
return nil
|
|
}
|
|
return RDEPUBHighlight(
|
|
id: id,
|
|
bookIdentifier: bookIdentifier,
|
|
location: location,
|
|
text: text,
|
|
rangeInfo: rangeInfo,
|
|
style: kind == .underline ? .underline : .highlight,
|
|
color: color ?? "#F8E16C",
|
|
note: note,
|
|
createdAt: createdAt
|
|
)
|
|
}
|
|
}
|