import Foundation import Quartz import UniformTypeIdentifiers /// **Space in Finder, on a `.kanban` package** — the whole of this extension's job. /// /// A board is a folder, and a folder previews as a folder: an icon and nothing else. This gives it a /// document's preview instead — its name, its tint, its lanes in order, each lane's card count and /// its first few card titles — built from `BoardOutline.read`, the capped read-only walk that lives /// beside the storage format it reads (Kanban/Storage/BoardOutline.swift). /// /// ### Data-based, not view-based /// /// `QLPreviewProvider` is the data-based half of the Quick Look preview API: the extension answers /// with *content* (`QLPreviewReply`) and Quick Look owns the window. The view-based half /// (`QLPreviewingController` on an `NSViewController`) would put a SwiftUI hierarchy on screen, and /// this preview has no use for one — it is static by design (v1: no interactivity, no attachment or /// image loading), so a view controller would be a lifecycle to keep honest in exchange for nothing. /// /// The reply is **HTML**, which is the one supported data type that reflows: a Quick Look panel is /// resized by the user and a board summary is a wrapping row of columns, so a fixed `contentSize` /// drawing block or a rendered image would be the wrong shape the moment the panel is not the size /// it was baked at. HTML also gets vector text, selectable text, its own scrolling, and light/dark /// through `prefers-color-scheme` — all of which a `CGContext` reply would have to reinvent. /// /// ### Where the work happens /// /// Everything is inside the reply's data-creation block, which is where Apple's own documentation /// puts it ("Heavy lifting should be done inside of the dataCreationBlock instead of when creating /// the QLPreviewReply"): `providePreview` returns immediately with a size hint, Quick Look draws its /// loading state at the right size, and the walk runs while it does. /// /// ### Reading the board /// /// The extension is sandboxed with read access to the URL it was handed and nothing else, which is /// exactly what the walk needs — it opens `index.md` files under `request.fileURL` and never looks /// outside the package. `BoardOutline.read` does not throw: a folder that is not a board, a board /// with a broken `index.md`, a permissions race mid-walk all yield an outline of whatever *did* read, /// so this method has no error path of its own and Quick Look never sees a failed preview where it /// could have shown a name. final class BoardPreviewProvider: QLPreviewProvider, QLPreviewingController { func providePreview(for request: QLFilePreviewRequest) async throws -> QLPreviewReply { let boardRoot = request.fileURL return QLPreviewReply( dataOfContentType: .html, contentSize: BoardPreviewPage.contentSizeHint ) { reply in let outline = BoardOutline.read(boardRoot: boardRoot) reply.stringEncoding = .utf8 // The panel's own title bar. Left empty, Quick Look uses the file name — which is the // folder name, and therefore *wrong* for every board whose `title:` differs from it. reply.title = outline.title reply.attachments = BoardPreviewPage.attachments(for: outline) return BoardPreviewPage.html(for: outline) } } }