Show images in Files and add client_open to open files on a client Mac

This commit is contained in:
Ian Gulliver
2026-10-19 19:46:24 -07:00
parent b72b4eda7f
commit 6ac297d374
3 changed files with 137 additions and 10 deletions
+69 -3
View File
@@ -44,6 +44,12 @@ private func bytes(_ n: Int64) -> String {
return "\((Double(n) / 1024 / 1024).formatted(.number.precision(.fractionLength(1)).grouping(.never))) MiB"
}
private let imageExts: Set<String> = ["png", "jpg", "jpeg", "gif", "webp", "svg"]
private func isImage(_ path: String) -> Bool {
imageExts.contains((path as NSString).pathExtension.lowercased())
}
private func shown(_ c: FileContent) -> Int64 {
Int64(c.text.utf8.count)
}
@@ -56,6 +62,7 @@ private final class FilesModel {
private(set) var content: FileContent?
private(set) var error = ""
private(set) var generation = 0
private(set) var loads = 0
@ObservationIgnored private var target = 0
@ObservationIgnored private var timer: Task<Void, Never>?
@@ -104,6 +111,7 @@ private final class FilesModel {
if started == target {
content = fresh
listing = nil
loads += 1
}
} else {
let fresh = try await store.api.client.getTaskDir(path: .init(taskId: taskId), query: .init(path: p.dir)).ok.body.json
@@ -142,6 +150,9 @@ private final class FilesModel {
return ""
}
if let c = content {
if place.file.map(isImage) == true {
return bytes(c.size)
}
if c.binary {
return "\(bytes(c.size)) · binary"
}
@@ -229,11 +240,14 @@ private struct FilesBody: View {
EmptyText(text: "No task selected.")
} else if !model.error.isEmpty {
EmptyText(text: model.error, error: true)
} else if let c = model.content {
if c.binary {
} else if let c = model.content, let taskId = store.selectedTaskId {
if let file = model.place.file, isImage(file) {
FileImage(taskId: taskId, path: file, load: model.loads)
.id(model.generation)
} else if c.binary {
EmptyText(text: "Binary file.")
} else if model.place.file?.lowercased().hasSuffix(".md") == true {
FileMarkdown(content: c, taskId: store.selectedTaskId, dir: model.place.dir)
FileMarkdown(content: c, taskId: taskId, dir: model.place.dir)
.id(model.generation)
} else {
FileText(content: c)
@@ -340,6 +354,58 @@ private struct FileMarkdown: View {
}
}
private struct FileImage: View {
let taskId: String
let path: String
let load: Int
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@ViewState private var image: NSImage?
@ViewState private var error = ""
var body: some View {
Group {
if let image {
ScrollView {
Image(nsImage: image)
.resizable()
.scaledToFit()
.frame(maxWidth: image.size.width, maxHeight: image.size.height)
.overlay(Rectangle().stroke(Palette.border))
.onTapGesture {
overlays.image = image
}
.help("Open full size")
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
} else if !error.isEmpty {
EmptyText(text: error, error: true)
} else {
EmptyText(text: "Loading…")
}
}
.task(id: load) {
do {
let data = try await ImageCache.fetch(.worktree(taskId: taskId, path: path), store.api)
guard let img = NSImage(data: data) else {
image = nil
error = "Not a readable image."
return
}
image = img
error = ""
} catch {
if Task.isCancelled {
return
}
image = nil
self.error = errorText(error)
}
}
}
}
private struct FileText: View {
let content: FileContent