osx: implements quicklook
This commit is contained in:
@@ -115,7 +115,15 @@ enum Display
|
||||
var previewPath: String {
|
||||
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
|
||||
|
||||
39
osx/kordophone2/Transcript/PreviewPanel.swift
Normal file
39
osx/kordophone2/Transcript/PreviewPanel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,8 @@ 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,17 +103,36 @@ struct ImageItemView: View
|
||||
var body: some View {
|
||||
BubbleView(sender: sender, date: date) {
|
||||
let maxWidth = CGFloat.minimum(.imageMaxWidth, containerWidth ?? .imageMaxWidth)
|
||||
if let img {
|
||||
Image(nsImage: img)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: maxWidth)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.gray.opacity(0.4))
|
||||
.frame(width: preferredWidth, height: preferredWidth / aspectRatio)
|
||||
.frame(maxWidth: maxWidth)
|
||||
|
||||
Group {
|
||||
if let img {
|
||||
Image(nsImage: img)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: maxWidth)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.gray.opacity(0.4))
|
||||
.frame(width: preferredWidth, height: preferredWidth / aspectRatio)
|
||||
.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 },
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
@@ -200,6 +220,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
|
||||
{
|
||||
@@ -209,7 +237,7 @@ final class XPCClient
|
||||
case connectionError
|
||||
}
|
||||
|
||||
enum Signal
|
||||
enum Signal: Equatable
|
||||
{
|
||||
case conversationsUpdated
|
||||
case messagesUpdated(conversationId: String)
|
||||
|
||||
@@ -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? {
|
||||
|
||||
Reference in New Issue
Block a user