Add a Save to Mac button for files in the Files tab
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import ByreAPI
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
typealias DirListing = Components.Schemas.DirListing
|
||||
typealias DirEntry = Components.Schemas.DirEntry
|
||||
@@ -95,6 +96,8 @@ private final class FilesModel {
|
||||
private(set) var error = ""
|
||||
private(set) var generation = 0
|
||||
private(set) var loads = 0
|
||||
var saving: FileCopy?
|
||||
var saveError = ""
|
||||
@ObservationIgnored private var target = 0
|
||||
@ObservationIgnored private var timer: Task<Void, Never>?
|
||||
|
||||
@@ -113,12 +116,21 @@ private final class FilesModel {
|
||||
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() {
|
||||
target += 1
|
||||
timer?.cancel()
|
||||
listing = nil
|
||||
content = nil
|
||||
error = ""
|
||||
saveError = ""
|
||||
generation += 1
|
||||
Task {
|
||||
await load()
|
||||
@@ -206,6 +218,12 @@ struct FilesView: View {
|
||||
if let model {
|
||||
FilesHead(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)
|
||||
@@ -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 {
|
||||
let model: FilesModel
|
||||
|
||||
@@ -257,11 +306,18 @@ private struct FilesHead: View {
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
Text(model.summary)
|
||||
Text(model.saveError.isEmpty ? model.summary : model.saveError)
|
||||
.font(Fonts.font(11))
|
||||
.foregroundStyle(Palette.muted)
|
||||
.foregroundStyle(model.saveError.isEmpty ? Palette.muted : Palette.danger)
|
||||
.lineLimit(1)
|
||||
.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())
|
||||
}
|
||||
@@ -307,13 +363,13 @@ private struct FilesBody: View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
ForEach(l.entries, id: \.name) { e in
|
||||
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 {
|
||||
model.go(FilePlace(dir: join(l.path, e.name)))
|
||||
} 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 meta: String
|
||||
let symlink: Bool
|
||||
let save: (() -> Void)?
|
||||
let action: () -> Void
|
||||
@ViewState private var hover = false
|
||||
|
||||
@@ -380,6 +437,11 @@ private struct EntryRow: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onHover { hover = $0 }
|
||||
.contextMenu {
|
||||
if let save {
|
||||
Button("Save Copy…", action: save)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user