Add a Save to Mac button for files in the Files tab

This commit is contained in:
Ian Gulliver
2026-10-23 12:17:17 -07:00
parent 1383bd6ae5
commit dc2fcc6075
4 changed files with 240 additions and 95 deletions
+17
View File
@@ -520,6 +520,23 @@ paths:
"400": { $ref: "#/components/responses/Error" } "400": { $ref: "#/components/responses/Error" }
"404": { $ref: "#/components/responses/Error" } "404": { $ref: "#/components/responses/Error" }
/api/projects/{projectId}/raw:
parameters:
- { name: projectId, in: path, required: true, schema: { type: string } }
get:
operationId: getProjectRaw
description: >
A regular file on the server, whole, with a content type chosen by its extension, as getTaskRaw serves it.
parameters:
- { name: path, in: query, required: true, schema: { type: string }, description: "Slash-separated, absolute or relative to the project's repository root." }
responses:
"200":
description: The file's bytes.
content:
"*/*": { schema: { type: string, format: binary } }
"400": { $ref: "#/components/responses/Error" }
"404": { $ref: "#/components/responses/Error" }
/api/projects/{projectId}/todo/tasks: /api/projects/{projectId}/todo/tasks:
parameters: parameters:
- { name: projectId, in: path, required: true, schema: { type: string } } - { name: projectId, in: path, required: true, schema: { type: string } }
+141 -88
View File
@@ -1924,6 +1924,12 @@ type GetProjectImageParams struct {
Path string `form:"path" json:"path"` Path string `form:"path" json:"path"`
} }
// GetProjectRawParams defines parameters for GetProjectRaw.
type GetProjectRawParams struct {
// Path Slash-separated, absolute or relative to the project's repository root.
Path string `form:"path" json:"path"`
}
// GetTaskChangesParams defines parameters for GetTaskChanges. // GetTaskChangesParams defines parameters for GetTaskChanges.
type GetTaskChangesParams struct { type GetTaskChangesParams struct {
// Since A commit to compare with instead of the merge-base, such as a user.message event's head. Not allowed with snapshot. // Since A commit to compare with instead of the merge-base, such as a user.message event's head. Not allowed with snapshot.
@@ -2230,6 +2236,9 @@ type ServerInterface interface {
// (GET /api/projects/{projectId}/image) // (GET /api/projects/{projectId}/image)
GetProjectImage(w http.ResponseWriter, r *http.Request, projectId string, params GetProjectImageParams) GetProjectImage(w http.ResponseWriter, r *http.Request, projectId string, params GetProjectImageParams)
// (GET /api/projects/{projectId}/raw)
GetProjectRaw(w http.ResponseWriter, r *http.Request, projectId string, params GetProjectRawParams)
// (PUT /api/projects/{projectId}/review-workflow) // (PUT /api/projects/{projectId}/review-workflow)
SetReviewWorkflow(w http.ResponseWriter, r *http.Request, projectId string) SetReviewWorkflow(w http.ResponseWriter, r *http.Request, projectId string)
@@ -3339,6 +3348,48 @@ func (siw *ServerInterfaceWrapper) GetProjectImage(w http.ResponseWriter, r *htt
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
} }
// GetProjectRaw operation middleware
func (siw *ServerInterfaceWrapper) GetProjectRaw(w http.ResponseWriter, r *http.Request) {
var err error
_ = err
// ------------- Path parameter "projectId" -------------
var projectId string
err = runtime.BindStyledParameterWithOptions("simple", "projectId", r.PathValue("projectId"), &projectId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectId", Err: err})
return
}
// Parameter object where we will unmarshal all parameters from the context
var params GetProjectRawParams
// ------------- Required query parameter "path" -------------
err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), &params.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
if err != nil {
var requiredError *runtime.RequiredParameterError
if errors.As(err, &requiredError) {
siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"})
} else {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err})
}
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetProjectRaw(w, r, projectId, params)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// SetReviewWorkflow operation middleware // SetReviewWorkflow operation middleware
func (siw *ServerInterfaceWrapper) SetReviewWorkflow(w http.ResponseWriter, r *http.Request) { func (siw *ServerInterfaceWrapper) SetReviewWorkflow(w http.ResponseWriter, r *http.Request) {
@@ -5037,6 +5088,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/dir", wrapper.GetProjectDir) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/dir", wrapper.GetProjectDir)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/file", wrapper.GetProjectFile) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/file", wrapper.GetProjectFile)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/image", wrapper.GetProjectImage) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/image", wrapper.GetProjectImage)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/raw", wrapper.GetProjectRaw)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/todo/tasks", wrapper.StartTodoItem) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/todo/tasks", wrapper.StartTodoItem)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}", wrapper.GetTask) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}", wrapper.GetTask)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/workflow", wrapper.GetTaskWorkflow) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/workflow", wrapper.GetTaskWorkflow)
@@ -5385,94 +5437,95 @@ var swaggerSpec = []string{
"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",
} }
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+16 -3
View File
@@ -228,11 +228,24 @@ func (s *Server) GetTaskRaw(w http.ResponseWriter, r *http.Request, taskID strin
if !ok { if !ok {
return return
} }
if params.Path == "" { serveRaw(w, r, t.WorktreePath, params.Path)
}
func (s *Server) GetProjectRaw(w http.ResponseWriter, r *http.Request, projectID string, params api.GetProjectRawParams) {
p, err := s.store.GetProject(r.Context(), projectID)
if err != nil {
writeStoreError(w, err)
return
}
serveRaw(w, r, p.RepoPath, params.Path)
}
func serveRaw(w http.ResponseWriter, r *http.Request, root, rel string) {
if rel == "" {
writeError(w, http.StatusBadRequest, errors.New("path is required")) writeError(w, http.StatusBadRequest, errors.New("path is required"))
return return
} }
name, full := taskPath(t.WorktreePath, params.Path) name, full := taskPath(root, rel)
f, err := os.Open(full) f, err := os.Open(full)
if err != nil { if err != nil {
@@ -247,7 +260,7 @@ func (s *Server) GetTaskRaw(w http.ResponseWriter, r *http.Request, taskID strin
return return
} }
if !fi.Mode().IsRegular() { if !fi.Mode().IsRegular() {
writeError(w, http.StatusBadRequest, fmt.Errorf("%s is not a regular file", params.Path)) writeError(w, http.StatusBadRequest, fmt.Errorf("%s is not a regular file", rel))
return return
} }
+66 -4
View File
@@ -1,5 +1,6 @@
import ByreAPI import ByreAPI
import SwiftUI import SwiftUI
import UniformTypeIdentifiers
typealias DirListing = Components.Schemas.DirListing typealias DirListing = Components.Schemas.DirListing
typealias DirEntry = Components.Schemas.DirEntry typealias DirEntry = Components.Schemas.DirEntry
@@ -95,6 +96,8 @@ private final class FilesModel {
private(set) var error = "" private(set) var error = ""
private(set) var generation = 0 private(set) var generation = 0
private(set) var loads = 0 private(set) var loads = 0
var saving: FileCopy?
var saveError = ""
@ObservationIgnored private var target = 0 @ObservationIgnored private var target = 0
@ObservationIgnored private var timer: Task<Void, Never>? @ObservationIgnored private var timer: Task<Void, Never>?
@@ -113,12 +116,21 @@ private final class FilesModel {
store.filePlaces[root] = p store.filePlaces[root] = p
} }
func save(_ path: String) {
guard let root else {
return
}
saveError = ""
saving = FileCopy(api: store.api, root: root, path: path)
}
func reset() { func reset() {
target += 1 target += 1
timer?.cancel() timer?.cancel()
listing = nil listing = nil
content = nil content = nil
error = "" error = ""
saveError = ""
generation += 1 generation += 1
Task { Task {
await load() await load()
@@ -206,6 +218,12 @@ struct FilesView: View {
if let model { if let model {
FilesHead(model: model) FilesHead(model: model)
FilesBody(model: model) FilesBody(model: model)
.fileExporter(isPresented: Binding { model.saving != nil } set: { if !$0 { model.saving = nil } }, item: model.saving, contentTypes: [.data], defaultFilename: model.saving.map { ($0.path as NSString).lastPathComponent }) { result in
model.saving = nil
if case .failure(let error) = result {
model.saveError = errorText(error)
}
}
} }
} }
.frame(maxHeight: .infinity, alignment: .top) .frame(maxHeight: .infinity, alignment: .top)
@@ -236,6 +254,37 @@ struct FilesView: View {
} }
} }
private struct FileCopy: Transferable, Sendable {
let api: API
let root: FileRoot
let path: String
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(exportedContentType: .data) { copy in
SentTransferredFile(try await copy.download())
}
}
private func download() async throws -> URL {
let body = switch root {
case .task(let id): try await api.client.getTaskRaw(path: .init(taskId: id), query: .init(path: path)).ok.body.any
case .project(let id): try await api.client.getProjectRaw(path: .init(projectId: id), query: .init(path: path)).ok.body.any
}
let dir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let dest = dir.appending(path: (path as NSString).lastPathComponent)
FileManager.default.createFile(atPath: dest.path, contents: nil)
let h = try FileHandle(forWritingTo: dest)
defer {
try? h.close()
}
for try await chunk in body {
try h.write(contentsOf: chunk)
}
return dest
}
}
private struct FilesHead: View { private struct FilesHead: View {
let model: FilesModel let model: FilesModel
@@ -257,11 +306,18 @@ private struct FilesHead: View {
} }
} }
Spacer(minLength: 0) Spacer(minLength: 0)
Text(model.summary) Text(model.saveError.isEmpty ? model.summary : model.saveError)
.font(Fonts.font(11)) .font(Fonts.font(11))
.foregroundStyle(Palette.muted) .foregroundStyle(model.saveError.isEmpty ? Palette.muted : Palette.danger)
.lineLimit(1) .lineLimit(1)
.fixedSize() .fixedSize()
if let file = p.file, model.content != nil {
Button("Save Copy…") {
model.save(file)
}
.buttonStyle(WebButtonStyle())
.help("Save a copy of this file on this device")
}
} }
.modifier(HeaderBar()) .modifier(HeaderBar())
} }
@@ -307,13 +363,13 @@ private struct FilesBody: View {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
if !l.path.isEmpty && l.path != "/" { if !l.path.isEmpty && l.path != "/" {
EntryRow(label: "../", kind: .dir, ignored: false, meta: "", symlink: false) { EntryRow(label: "../", kind: .dir, ignored: false, meta: "", symlink: false, save: nil) {
model.go(FilePlace(dir: parent(l.path))) model.go(FilePlace(dir: parent(l.path)))
} }
} }
ForEach(l.entries, id: \.name) { e in ForEach(l.entries, id: \.name) { e in
let meta = [e.ignored ? "ignored" : nil, e._type == .file ? bytes(e.size) : nil].compactMap { $0 }.joined(separator: " · ") let meta = [e.ignored ? "ignored" : nil, e._type == .file ? bytes(e.size) : nil].compactMap { $0 }.joined(separator: " · ")
EntryRow(label: e._type == .dir ? "\(e.name)/" : e.name, kind: e._type, ignored: e.ignored, meta: meta, symlink: e.symlink) { EntryRow(label: e._type == .dir ? "\(e.name)/" : e.name, kind: e._type, ignored: e.ignored, meta: meta, symlink: e.symlink, save: e._type == .file ? { model.save(join(l.path, e.name)) } : nil) {
if e._type == .dir { if e._type == .dir {
model.go(FilePlace(dir: join(l.path, e.name))) model.go(FilePlace(dir: join(l.path, e.name)))
} else if e._type == .file && isPage(e.name), case .task(let taskId) = model.root { } else if e._type == .file && isPage(e.name), case .task(let taskId) = model.root {
@@ -349,6 +405,7 @@ private struct EntryRow: View {
let ignored: Bool let ignored: Bool
let meta: String let meta: String
let symlink: Bool let symlink: Bool
let save: (() -> Void)?
let action: () -> Void let action: () -> Void
@ViewState private var hover = false @ViewState private var hover = false
@@ -380,6 +437,11 @@ private struct EntryRow: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.onHover { hover = $0 } .onHover { hover = $0 }
.contextMenu {
if let save {
Button("Save Copy…", action: save)
}
}
} }
} }
} }