osx: implements quicklook
This commit is contained in:
@@ -116,6 +116,14 @@ enum Display
|
|||||||
data.previewPath
|
data.previewPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isFullsizeDownloaded: Bool {
|
||||||
|
data.isDownloaded
|
||||||
|
}
|
||||||
|
|
||||||
|
var fullsizePath: String {
|
||||||
|
data.path
|
||||||
|
}
|
||||||
|
|
||||||
init(from serialized: Serialized.Attachment, dateSent: Date, sender: Sender) {
|
init(from serialized: Serialized.Attachment, dateSent: Date, sender: Sender) {
|
||||||
self.id = serialized.guid
|
self.id = serialized.guid
|
||||||
self.sender = sender
|
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,6 +90,7 @@ struct ImageItemView: View
|
|||||||
@Environment(\.xpcClient) var xpcClient
|
@Environment(\.xpcClient) var xpcClient
|
||||||
|
|
||||||
@State private var containerWidth: CGFloat? = nil
|
@State private var containerWidth: CGFloat? = nil
|
||||||
|
@State private var isDownloadingFullAttachment: Bool = false
|
||||||
|
|
||||||
private var aspectRatio: CGFloat {
|
private var aspectRatio: CGFloat {
|
||||||
attachment.size?.aspectRatio ?? 1.0
|
attachment.size?.aspectRatio ?? 1.0
|
||||||
@@ -102,6 +103,8 @@ struct ImageItemView: View
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
BubbleView(sender: sender, date: date) {
|
BubbleView(sender: sender, date: date) {
|
||||||
let maxWidth = CGFloat.minimum(.imageMaxWidth, containerWidth ?? .imageMaxWidth)
|
let maxWidth = CGFloat.minimum(.imageMaxWidth, containerWidth ?? .imageMaxWidth)
|
||||||
|
|
||||||
|
Group {
|
||||||
if let img {
|
if let img {
|
||||||
Image(nsImage: img)
|
Image(nsImage: img)
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -114,6 +117,23 @@ struct ImageItemView: View
|
|||||||
.frame(maxWidth: maxWidth)
|
.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,
|
.onGeometryChange(for: CGFloat.self,
|
||||||
of: { $0.size.width },
|
of: { $0.size.width },
|
||||||
action: { containerWidth = $0 })
|
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
|
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 }
|
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] = [:]
|
var args: [String: xpc_object_t] = [:]
|
||||||
args["attachment_id"] = xpcString(attachmentId)
|
args["attachment_id"] = xpcString(attachmentId)
|
||||||
args["preview"] = xpcString(preview ? "true" : "false")
|
args["preview"] = xpcString(preview ? "true" : "false")
|
||||||
|
|
||||||
let req = makeRequest(method: "DownloadAttachment", arguments: args)
|
let req = makeRequest(method: "DownloadAttachment", arguments: args)
|
||||||
_ = try await sendSync(req)
|
_ = 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 {
|
public func uploadAttachment(path: String) async throws -> String {
|
||||||
@@ -201,6 +221,14 @@ final class XPCClient
|
|||||||
|
|
||||||
// MARK: - Types
|
// MARK: - Types
|
||||||
|
|
||||||
|
struct AttachmentInfo: Decodable
|
||||||
|
{
|
||||||
|
let path: String
|
||||||
|
let previewPath: String
|
||||||
|
let isDownloaded: Bool
|
||||||
|
let isPreviewDownloaded: Bool
|
||||||
|
}
|
||||||
|
|
||||||
enum Error: Swift.Error
|
enum Error: Swift.Error
|
||||||
{
|
{
|
||||||
case typeError
|
case typeError
|
||||||
@@ -209,7 +237,7 @@ final class XPCClient
|
|||||||
case connectionError
|
case connectionError
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Signal
|
enum Signal: Equatable
|
||||||
{
|
{
|
||||||
case conversationsUpdated
|
case conversationsUpdated
|
||||||
case messagesUpdated(conversationId: String)
|
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
|
extension xpc_object_t
|
||||||
{
|
{
|
||||||
func getObject(_ key: String) -> xpc_object_t? {
|
func getObject(_ key: String) -> xpc_object_t? {
|
||||||
|
|||||||
Reference in New Issue
Block a user