Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: add remote folder picker to file sync GUI#127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
ethanndickson merged 6 commits intomainfromethan/remote-file-picker
Apr 9, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletionsCoder-Desktop/Coder-Desktop/Info.plist
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,15 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<!--
Required to make HTTP (not HTTPS) requests to workspace agents
(i.e. workspace.coder:4). These are already encrypted over wireguard.
-->
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NetworkExtension</key>
<dict>
<key>NEMachServiceName</key>
Expand Down
232 changes: 232 additions & 0 deletionsCoder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
import CoderSDK
import Foundation
import SwiftUI

struct FilePicker: View {
@Environment(\.dismiss) var dismiss
@StateObject private var model: FilePickerModel
@State private var selection: FilePickerEntryModel?

@Binding var outputAbsPath: String

let inspection = Inspection<Self>()

init(
host: String,
outputAbsPath: Binding<String>
) {
_model = StateObject(wrappedValue: FilePickerModel(host: host))
_outputAbsPath = outputAbsPath
}

var body: some View {
VStack(spacing: 0) {
if model.rootIsLoading {
Spacer()
ProgressView()
.controlSize(.large)
Spacer()
} else if let loadError = model.error {
Text("\(loadError.description)")
.font(.headline)
.foregroundColor(.red)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
} else {
List(selection: $selection) {
ForEach(model.rootEntries) { entry in
FilePickerEntry(entry: entry).tag(entry)
}
}.contextMenu(
forSelectionType: FilePickerEntryModel.self,
menu: { _ in },
primaryAction: { selections in
// Per the type of `selection`, this will only ever be a set of
// one entry.
selections.forEach { entry in withAnimation { entry.isExpanded.toggle() } }
}
).listStyle(.sidebar)
}
Divider()
HStack {
Spacer()
Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
Button("Select", action: submit).keyboardShortcut(.defaultAction).disabled(selection == nil)
}.padding(20)
}
.onAppear {
model.loadRoot()
}
.onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
}

private func submit() {
guard let selection else { return }
outputAbsPath = selection.absolute_path
dismiss()
}
}

@MainActor
class FilePickerModel: ObservableObject {
@Published var rootEntries: [FilePickerEntryModel] = []
@Published var rootIsLoading: Bool = false
@Published var error: ClientError?

// It's important that `AgentClient` is a reference type (class)
// as we were having performance issues with a struct (unless it was a binding).
let client: AgentClient

init(host: String) {
client = AgentClient(agentHost: host)
}

func loadRoot() {
error = nil
rootIsLoading = true
Task {
defer { rootIsLoading = false }
do throws(ClientError) {
rootEntries = try await client
.listAgentDirectory(.init(path: [], relativity: .root))
.toModels(client: client)
} catch {
self.error = error
}
}
}
}

struct FilePickerEntry: View {
@ObservedObject var entry: FilePickerEntryModel

var body: some View {
Group {
if entry.dir {
directory
} else {
Label(entry.name, systemImage: "doc")
.help(entry.absolute_path)
.selectionDisabled()
.foregroundColor(.secondary)
}
}
}

private var directory: some View {
DisclosureGroup(isExpanded: $entry.isExpanded) {
if let entries = entry.entries {
ForEach(entries) { entry in
FilePickerEntry(entry: entry).tag(entry)
}
}
} label: {
Label {
Text(entry.name)
ZStack {
ProgressView().controlSize(.small).opacity(entry.isLoading && entry.error == nil ? 1 : 0)
Image(systemName: "exclamationmark.triangle.fill")
.opacity(entry.error != nil ? 1 : 0)
}
} icon: {
Image(systemName: "folder")
}.help(entry.error != nil ? entry.error!.description : entry.absolute_path)
}
}
}

@MainActor
class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
nonisolated let id: [String]
let name: String
// Components of the path as an array
let path: [String]
let absolute_path: String
let dir: Bool

let client: AgentClient

@Published var entries: [FilePickerEntryModel]?
@Published var isLoading = false
@Published var error: ClientError?
@Published private var innerIsExpanded = false
var isExpanded: Bool {
get { innerIsExpanded }
set {
if !newValue {
withAnimation { self.innerIsExpanded = false }
} else {
Task {
self.loadEntries()
}
}
}
}

init(
name: String,
client: AgentClient,
absolute_path: String,
path: [String],
dir: Bool = false,
entries: [FilePickerEntryModel]? = nil
) {
self.name = name
self.client = client
self.path = path
self.dir = dir
self.absolute_path = absolute_path
self.entries = entries

// Swift Arrays are copy on write
id = path
}

func loadEntries() {
self.error = nil
withAnimation { isLoading = true }
Task {
defer {
withAnimation {
isLoading = false
innerIsExpanded = true
}
}
do throws(ClientError) {
entries = try await client
.listAgentDirectory(.init(path: path, relativity: .root))
.toModels(client: client)
} catch {
self.error = error
}
}
}

nonisolated static func == (lhs: FilePickerEntryModel, rhs: FilePickerEntryModel) -> Bool {
lhs.id == rhs.id
}

nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

extension LSResponse {
@MainActor
func toModels(client: AgentClient) -> [FilePickerEntryModel] {
contents.compactMap { entry in
// Filter dotfiles from the picker
guard !entry.name.hasPrefix(".") else { return nil }

return FilePickerEntryModel(
name: entry.name,
client: client,
absolute_path: entry.absolute_path_string,
path: self.absolute_path + [entry.name],
dir: entry.is_dir,
entries: nil
)
}
}
}
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {

@State private var loading: Bool = false
@State private var createError: DaemonError?
@State private var pickingRemote: Bool = false

var body: some View {
let agents = vpn.menuState.onlineAgents
Expand DownExpand Up@@ -46,7 +47,16 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
}
}
Section {
TextField("Remote Path", text: $remotePath)
HStack(spacing: 5) {
TextField("Remote Path", text: $remotePath)
Spacer()
Button {
pickingRemote = true
} label: {
Image(systemName: "folder")
}.disabled(remoteHostname == nil)
.help(remoteHostname == nil ? "Select a workspace first" : "Open File Picker")
}
}
}.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
Divider()
Expand All@@ -72,6 +82,9 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
set: { if !$0 { createError = nil } }
)) {} message: {
Text(createError?.description ?? "An unknown error occurred.")
}.sheet(isPresented: $pickingRemote) {
FilePicker(host: remoteHostname!, outputAbsPath: $remotePath)
.frame(width: 300, height: 400)
}
}

Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp