Prepare v0.4 release and open source docs

This commit is contained in:
Akshay Kolli
2026-06-29 23:42:39 -07:00
parent 085d7a16dc
commit 504bd2d39a
58 changed files with 5076 additions and 923 deletions

View File

@@ -103,17 +103,21 @@ public enum AnnotationFactory {
date: Date = Date()
) -> [AnnotationInsertion] {
let lineSelections = selection.selectionsByLine()
var groups: [(page: PDFPage, rects: [CGRect])] = []
var groups: [(page: PDFPage, rects: [CGRect], text: [String])] = []
for lineSelection in lineSelections {
let lineText = lineSelection.string?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
for page in lineSelection.pages {
let rect = lineSelection.bounds(for: page).insetBy(dx: -1.5, dy: -1.0)
guard !rect.isNull, rect.width > 0, rect.height > 0 else { continue }
if let index = groups.firstIndex(where: { $0.page === page }) {
groups[index].rects.append(rect)
if !lineText.isEmpty {
groups[index].text.append(lineText)
}
} else {
groups.append((page: page, rects: [rect]))
groups.append((page: page, rects: [rect], text: lineText.isEmpty ? [] : [lineText]))
}
}
}
@@ -130,6 +134,12 @@ public enum AnnotationFactory {
quadPoints(for: rect, relativeTo: unionRect)
}
standardize(annotation, comment: comment, author: author, date: date)
if style == .highlight {
let highlightText = group.text.joined(separator: " ")
if !highlightText.isEmpty {
_ = annotation.setValue(highlightText, forAnnotationKey: AnnotationKeys.appHighlightText)
}
}
if style == .comment {
_ = annotation.setValue(AnnotationKeys.appKindComment, forAnnotationKey: AnnotationKeys.appKind)
}

View File

@@ -2,7 +2,7 @@ import AppKit
import Foundation
import PDFKit
public enum AcademicAnnotationKind: String, CaseIterable, Identifiable {
public enum AcademicAnnotationKind: String, CaseIterable {
case comment
case highlight
case underline
@@ -11,8 +11,6 @@ public enum AcademicAnnotationKind: String, CaseIterable, Identifiable {
case reply
case other
public var id: String { rawValue }
public init(annotation: PDFAnnotation) {
if annotation.value(forAnnotationKey: AnnotationKeys.appKind) as? String == AnnotationKeys.appKindComment {
self = .comment
@@ -62,7 +60,7 @@ public enum AcademicAnnotationKind: String, CaseIterable, Identifiable {
}
}
public struct AnnotationSnapshot: Identifiable, Equatable {
public struct AnnotationSnapshot: Identifiable {
public let id: String
public let pageIndex: Int
public let pageLabel: String
@@ -73,6 +71,7 @@ public struct AnnotationSnapshot: Identifiable, Equatable {
public let modifiedAt: Date?
public let status: String
public let contents: String
public let highlightText: String
public let bounds: CGRect
public let annotation: PDFAnnotation
public let page: PDFPage
@@ -89,6 +88,7 @@ public struct AnnotationSnapshot: Identifiable, Equatable {
modifiedAt: Date?,
status: String,
contents: String,
highlightText: String,
bounds: CGRect,
annotation: PDFAnnotation,
page: PDFPage,
@@ -104,6 +104,7 @@ public struct AnnotationSnapshot: Identifiable, Equatable {
self.modifiedAt = modifiedAt
self.status = status
self.contents = contents
self.highlightText = highlightText
self.bounds = bounds
self.annotation = annotation
self.page = page
@@ -126,23 +127,22 @@ public struct AnnotationSnapshot: Identifiable, Equatable {
!contents.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
public var isReply: Bool {
parentID != nil
public var highlightExcerpt: String {
let stored = highlightText.trimmingCharacters(in: .whitespacesAndNewlines)
if !stored.isEmpty {
return stored
}
let fallback = contents.trimmingCharacters(in: .whitespacesAndNewlines)
if !fallback.isEmpty {
return fallback
}
return "Highlight on page \(pageLabel)"
}
public static func == (lhs: AnnotationSnapshot, rhs: AnnotationSnapshot) -> Bool {
lhs.id == rhs.id
&& lhs.pageIndex == rhs.pageIndex
&& lhs.pageLabel == rhs.pageLabel
&& lhs.annotationIndex == rhs.annotationIndex
&& lhs.kind == rhs.kind
&& lhs.author == rhs.author
&& lhs.createdAt == rhs.createdAt
&& lhs.modifiedAt == rhs.modifiedAt
&& lhs.status == rhs.status
&& lhs.contents == rhs.contents
&& lhs.bounds == rhs.bounds
&& lhs.parentID == rhs.parentID
public var isReply: Bool {
parentID != nil
}
}
@@ -155,6 +155,7 @@ public enum AnnotationKeys {
public static let appKind = PDFAnnotationKey(rawValue: "IHatePDFsKind")
public static let appKindComment = "Comment"
public static let appCommentText = PDFAnnotationKey(rawValue: "IHatePDFsCommentText")
public static let appHighlightText = PDFAnnotationKey(rawValue: "IHatePDFsHighlightText")
public static func commentText(for annotation: PDFAnnotation) -> String {
if let value = annotation.value(forAnnotationKey: appCommentText) as? String,
@@ -351,6 +352,7 @@ public enum AnnotationReader {
let kind = AcademicAnnotationKind(annotation: annotation)
let contents = AnnotationKeys.commentText(for: annotation)
let highlightText = annotation.value(forAnnotationKey: AnnotationKeys.appHighlightText) as? String ?? ""
guard kind != .other || !contents.isEmpty else { continue }
let id = AnnotationKeys.stableID(
@@ -384,6 +386,7 @@ public enum AnnotationReader {
modifiedAt: annotation.modificationDate,
status: status,
contents: contents,
highlightText: highlightText,
bounds: annotation.bounds,
annotation: annotation,
page: page,

View File

@@ -0,0 +1,61 @@
import Foundation
public struct PDFDocumentBookmark {
public var id: String
public var pageIndex: Int
public var pageLabel: String
public var title: String
public var createdAt: Date
public init(
id: String = UUID().uuidString,
pageIndex: Int,
pageLabel: String,
title: String,
createdAt: Date = Date()
) {
self.id = id
self.pageIndex = pageIndex
self.pageLabel = pageLabel
self.title = title
self.createdAt = createdAt
}
}
public enum PDFDocumentBookmarks {
public static func sorted(_ bookmarks: [PDFDocumentBookmark]) -> [PDFDocumentBookmark] {
preferredBookmark(in: bookmarks).map { [$0] } ?? []
}
public static func upsert(
_ bookmark: PDFDocumentBookmark,
in _: [PDFDocumentBookmark]
) -> [PDFDocumentBookmark] {
[bookmark]
}
public static func removing(id: String, from bookmarks: [PDFDocumentBookmark]) -> [PDFDocumentBookmark] {
sorted(bookmarks.filter { $0.id != id })
}
public static func bookmark(on pageIndex: Int, in bookmarks: [PDFDocumentBookmark]) -> PDFDocumentBookmark? {
sorted(bookmarks).first { $0.pageIndex == pageIndex }
}
public static func clamped(
_ bookmarks: [PDFDocumentBookmark],
pageCount: Int
) -> [PDFDocumentBookmark] {
guard pageCount > 0 else { return [] }
return sorted(bookmarks.filter { (0..<pageCount).contains($0.pageIndex) })
}
private static func preferredBookmark(in bookmarks: [PDFDocumentBookmark]) -> PDFDocumentBookmark? {
bookmarks.max {
if $0.createdAt != $1.createdAt {
return $0.createdAt < $1.createdAt
}
return $0.pageIndex < $1.pageIndex
}
}
}

View File

@@ -0,0 +1,82 @@
import Foundation
public struct PDFRecentDocumentProgress {
public var key: String
public var pageIndex: Int
public var openedAt: Date
public init(key: String, pageIndex: Int, openedAt: Date) {
self.key = key
self.pageIndex = pageIndex
self.openedAt = openedAt
}
}
public enum PDFRecentDocuments {
public static func filteredPDFs(
from urls: [URL],
currentURL: URL? = nil,
limit: Int,
fileExists: (URL) -> Bool = { FileManager.default.fileExists(atPath: $0.path) }
) -> [URL] {
guard limit > 0 else { return [] }
var result: [URL] = []
var seen = Set<URL>()
let current = currentURL.map(normalized)
for url in urls {
let normalizedURL = normalized(url)
guard normalizedURL != current,
seen.insert(normalizedURL).inserted,
PDFFileSelection.isPDFFileURL(normalizedURL),
fileExists(normalizedURL)
else {
continue
}
result.append(normalizedURL)
if result.count == limit {
break
}
}
return result
}
public static func documentKey(for url: URL) -> String {
normalized(url).path
}
public static func progress(
for url: URL,
in records: [String: PDFRecentDocumentProgress]
) -> PDFRecentDocumentProgress? {
records[documentKey(for: url)]
}
public static func updatedProgress(
_ records: [String: PDFRecentDocumentProgress],
url: URL,
pageIndex: Int,
openedAt: Date
) -> [String: PDFRecentDocumentProgress] {
let key = documentKey(for: url)
var copy = records
copy[key] = PDFRecentDocumentProgress(
key: key,
pageIndex: max(0, pageIndex),
openedAt: openedAt
)
return copy
}
public static func clampedPageIndex(_ pageIndex: Int?, pageCount: Int) -> Int {
guard pageCount > 0, let pageIndex else { return 0 }
return min(max(0, pageIndex), pageCount - 1)
}
static func normalized(_ url: URL) -> URL {
url.standardizedFileURL
}
}

View File

@@ -7,10 +7,14 @@ public enum ReturnKeyCommitPolicy {
option: Bool,
command: Bool,
control: Bool,
isEditableMultilineText: Bool
isEditableMultilineText: Bool,
commandReturnOnly: Bool = false
) -> Bool {
guard isEditableMultilineText else { return false }
guard keyCode == 36 || keyCode == 76 else { return false }
if commandReturnOnly {
return command && !shift && !option && !control
}
return !shift && !option && !command && !control
}
}