Private
Public Access
1
0

osx: implements quicklook

This commit is contained in:
2025-09-12 18:17:58 -07:00
parent 87e986707d
commit 1a5f13f2b8
5 changed files with 143 additions and 14 deletions

View File

@@ -116,6 +116,14 @@ enum Display
data.previewPath
}
var isFullsizeDownloaded: Bool {
data.isDownloaded
}
var fullsizePath: String {
data.path
}
init(from serialized: Serialized.Attachment, dateSent: Date, sender: Sender) {
self.id = serialized.guid
self.sender = sender

View File

@@ -0,0 +1,39 @@
//
// PreviewPanel.swift
// Kordophone
//
// Created by James Magahern on 9/12/25.
//
import AppKit
import QuickLook
import QuickLookUI
internal class PreviewPanel
{
static let shared = PreviewPanel()
private var displayedURL: URL? = nil
private var impl: QLPreviewPanel { QLPreviewPanel.shared() }
private init() {
impl.dataSource = self
}
public func show(url: URL) {
self.displayedURL = url
impl.makeKeyAndOrderFront(self)
}
}
extension PreviewPanel: QLPreviewPanelDataSource
{
func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
1
}
func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> (any QLPreviewItem)! {
return displayedURL! as NSURL
}
}

View File

@@ -90,6 +90,7 @@ struct ImageItemView: View
@Environment(\.xpcClient) var xpcClient
@State private var containerWidth: CGFloat? = nil
@State private var isDownloadingFullAttachment: Bool = false
private var aspectRatio: CGFloat {
attachment.size?.aspectRatio ?? 1.0
@@ -102,6 +103,8 @@ struct ImageItemView: View
var body: some View {
BubbleView(sender: sender, date: date) {
let maxWidth = CGFloat.minimum(.imageMaxWidth, containerWidth ?? .imageMaxWidth)
Group {
if let img {
Image(nsImage: img)
.resizable()
@@ -114,6 +117,23 @@ struct ImageItemView: View
.frame(maxWidth: maxWidth)
}
}
// Download indicator
.overlay {
if isDownloadingFullAttachment {
ZStack {
Rectangle()
.fill(.black.opacity(0.2))
ProgressView()
.progressViewStyle(.circular)
}
}
}
}
.onTapGesture(count: 2) {
openAttachment()
}
.onGeometryChange(for: CGFloat.self,
of: { $0.size.width },
action: { containerWidth = $0 })
@@ -136,6 +156,24 @@ struct ImageItemView: View
}
}
}
private func openAttachment() {
Task {
var path = attachment.fullsizePath
if !attachment.isFullsizeDownloaded {
isDownloadingFullAttachment = true
try await xpcClient.downloadAttachment(attachmentId: attachment.id, preview: false, awaitCompletion: true)
// Need to re-fetch this -- the extension may have changed.
let info = try await xpcClient.getAttachmentInfo(attachmentId: attachment.id)
path = info.path
isDownloadingFullAttachment = false
}
PreviewPanel.shared.show(url: URL(filePath: path))
}
}
}
struct PlaceholderImageItemView: View

View File

@@ -146,13 +146,33 @@ final class XPCClient
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { throw Error.typeError }
}
public func downloadAttachment(attachmentId: String, preview: Bool) async throws {
public func getAttachmentInfo(attachmentId: String) async throws -> AttachmentInfo {
var args: [String: xpc_object_t] = [:]
args["attachment_id"] = xpcString(attachmentId)
let req = makeRequest(method: "GetAttachmentInfo", arguments: args)
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { throw Error.typeError }
return AttachmentInfo(
path: reply["path"] ?? "",
previewPath: reply["preview_path"] ?? "",
isDownloaded: reply["is_downloaded"] ?? false,
isPreviewDownloaded: reply["is_preview_downloaded"] ?? false
)
}
public func downloadAttachment(attachmentId: String, preview: Bool, awaitCompletion: Bool = false) async throws {
var args: [String: xpc_object_t] = [:]
args["attachment_id"] = xpcString(attachmentId)
args["preview"] = xpcString(preview ? "true" : "false")
let req = makeRequest(method: "DownloadAttachment", arguments: args)
_ = try await sendSync(req)
if awaitCompletion {
// Wait for downloaded event
let _ = await eventStream().first { $0 == .attachmentDownloaded(attachmentId: attachmentId) }
}
}
public func uploadAttachment(path: String) async throws -> String {
@@ -201,6 +221,14 @@ final class XPCClient
// MARK: - Types
struct AttachmentInfo: Decodable
{
let path: String
let previewPath: String
let isDownloaded: Bool
let isPreviewDownloaded: Bool
}
enum Error: Swift.Error
{
case typeError
@@ -209,7 +237,7 @@ final class XPCClient
case connectionError
}
enum Signal
enum Signal: Equatable
{
case conversationsUpdated
case messagesUpdated(conversationId: String)

View File

@@ -76,6 +76,22 @@ extension Array: XPCConvertible where Element: XPCConvertible
}
}
extension Bool: XPCConvertible
{
static func fromXPC(_ value: xpc_object_t) -> Bool? {
if xpc_get_type(value) == XPC_TYPE_BOOL {
return xpc_bool_get_value(value)
}
if xpc_get_type(value) == XPC_TYPE_STRING {
guard let cstr = xpc_string_get_string_ptr(value) else { return nil }
return strcmp(cstr, "true") == 0
}
return nil
}
}
extension xpc_object_t
{
func getObject(_ key: String) -> xpc_object_t? {