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

Commite425548

Browse files
committed
feat: add remote folder picker to file sync GUI
1 parentfe20801 commite425548

File tree

7 files changed

+463
-2
lines changed

7 files changed

+463
-2
lines changed

‎Coder-Desktop/Coder-Desktop/Info.plist

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
<!DOCTYPEplist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plistversion="1.0">
44
<dict>
5+
<key>NSAppTransportSecurity</key>
6+
<dict>
7+
<!--
8+
Required to make HTTP (not HTTPS) requests to workspace agents
9+
(i.e. workspace.coder:4). These are already encrypted over wireguard.
10+
-->
11+
<key>NSAllowsArbitraryLoads</key>
12+
<true/>
13+
</dict>
514
<key>NetworkExtension</key>
615
<dict>
716
<key>NEMachServiceName</key>
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import CoderSDK
2+
import Foundation
3+
import SwiftUI
4+
5+
structFilePicker:View{
6+
@Environment(\.dismiss)vardismiss
7+
@StateObjectprivatevarmodel:FilePickerModel
8+
@Stateprivatevarselection:FilePickerItemModel.ID?
9+
10+
@BindingvaroutputAbsPath:String
11+
12+
letinspection=Inspection<Self>()
13+
14+
init(
15+
host:String,
16+
outputAbsPath:Binding<String>
17+
){
18+
_model=StateObject(wrappedValue:FilePickerModel(host: host))
19+
_outputAbsPath= outputAbsPath
20+
}
21+
22+
varbody:someView{
23+
VStack(spacing:0){
24+
if model.isLoading{
25+
Spacer()
26+
ProgressView()
27+
.controlSize(.large)
28+
Spacer()
29+
}elseiflet loadError= model.error{
30+
Text("\(loadError.description)")
31+
.font(.headline)
32+
.foregroundColor(.red)
33+
.multilineTextAlignment(.center)
34+
.frame(maxWidth:.infinity, maxHeight:.infinity)
35+
.padding()
36+
}else{
37+
List(selection: $selection){
38+
ForEach(model.rootFiles){ rootItemin
39+
FilePickerItem(item: rootItem)
40+
}
41+
}.contextMenu(
42+
forSelectionType:FilePickerItemModel.ID.self,
43+
menu:{ _in},
44+
primaryAction:{ selectionsin
45+
// Per the type of `selection`, this will only ever be a set of
46+
// one item.
47+
letfiles= model.findFilesByIds(ids: selections)
48+
files.forEach{ fileinwithAnimation{ file.isExpanded.toggle()}}
49+
}
50+
).listStyle(.sidebar)
51+
}
52+
Divider()
53+
HStack{
54+
Spacer()
55+
Button("Cancel", action:{dismiss()}).keyboardShortcut(.cancelAction)
56+
Button("Select", action: submit).keyboardShortcut(.defaultAction).disabled(selection==nil)
57+
}.padding(20)
58+
}
59+
.onAppear{
60+
model.loadRoot()
61+
}
62+
.onReceive(inspection.notice){ inspection.visit(self, $0)} // ViewInspector
63+
}
64+
65+
privatefunc submit(){
66+
guardlet selectionelse{return}
67+
letfiles= model.findFilesByIds(ids:[selection])
68+
iflet file= files.first{
69+
outputAbsPath= file.absolute_path
70+
}
71+
dismiss()
72+
}
73+
}
74+
75+
@MainActor
76+
classFilePickerModel:ObservableObject{
77+
@PublishedvarrootFiles:[FilePickerItemModel]=[]
78+
@PublishedvarisLoading:Bool=false
79+
@Publishedvarerror:ClientError?
80+
81+
letclient:Client
82+
83+
init(host:String){
84+
client=Client(url:URL(string:"http://\(host):4")!)
85+
}
86+
87+
func loadRoot(){
88+
error=nil
89+
isLoading=true
90+
Task{
91+
defer{ isLoading=false}
92+
dothrows(ClientError){
93+
rootFiles=tryawait client
94+
.listAgentDirectory(.init(path:[], relativity:.root))
95+
.toModels(client:Binding(get:{self.client}, set:{ _in}), path:[])
96+
} catch{
97+
self.error= error
98+
}
99+
}
100+
}
101+
102+
func findFilesByIds(ids:Set<FilePickerItemModel.ID>)->[FilePickerItemModel]{
103+
varresult:[FilePickerItemModel]=[]
104+
105+
foridin ids{
106+
iflet file=findFileByPath(path: id, in: rootFiles){
107+
result.append(file)
108+
}
109+
}
110+
111+
return result
112+
}
113+
114+
privatefunc findFileByPath(path:[String], in files:[FilePickerItemModel]?)->FilePickerItemModel?{
115+
guardlet files, !path.isEmptyelse{returnnil}
116+
117+
iflet file= files.first(where:{ $0.name==path[0]}){
118+
if path.count==1{
119+
return file
120+
}
121+
// Array slices are just views, so this isn't expensive
122+
returnfindFileByPath(path:Array(path[1...]), in: file.contents)
123+
}
124+
125+
returnnil
126+
}
127+
}
128+
129+
structFilePickerItem:View{
130+
@ObservedObjectvaritem:FilePickerItemModel
131+
132+
varbody:someView{
133+
Group{
134+
if item.dir{
135+
directory
136+
}else{
137+
Label(item.name, systemImage:"doc")
138+
.help(item.absolute_path)
139+
.selectionDisabled()
140+
.foregroundColor(.secondary)
141+
}
142+
}
143+
}
144+
145+
privatevardirectory:someView{
146+
DisclosureGroup(isExpanded: $item.isExpanded){
147+
iflet contents= item.contents{
148+
ForEach(contents){ itemin
149+
FilePickerItem(item: item)
150+
}
151+
}
152+
} label:{
153+
Label{
154+
Text(item.name)
155+
ZStack{
156+
ProgressView().controlSize(.small).opacity(item.isLoading && item.error==nil?1:0)
157+
Image(systemName:"exclamationmark.triangle.fill")
158+
.opacity(item.error!=nil?1:0)
159+
}
160+
} icon:{
161+
Image(systemName:"folder")
162+
}.help(item.error!=nil? item.error!.description: item.absolute_path)
163+
}
164+
}
165+
}
166+
167+
@MainActor
168+
classFilePickerItemModel:Identifiable,ObservableObject{
169+
nonisolatedletid:[String]
170+
letname:String
171+
// Components of the path as an array
172+
letpath:[String]
173+
letabsolute_path:String
174+
letdir:Bool
175+
176+
// This being a binding is pretty important performance-wise, as it's a struct
177+
// that would otherwise be recreated every time the the item row is rendered.
178+
// Removing the binding results in very noticeable lag when scrolling a file tree.
179+
@Bindingvarclient:Client
180+
181+
@Publishedvarcontents:[FilePickerItemModel]?
182+
@PublishedvarisLoading=false
183+
@Publishedvarerror:ClientError?
184+
@PublishedprivatevarinnerIsExpanded=false
185+
varisExpanded:Bool{
186+
get{ innerIsExpanded}
187+
set{
188+
if !newValue{
189+
withAnimation{self.innerIsExpanded=false}
190+
}else{
191+
Task{
192+
self.loadContents()
193+
}
194+
}
195+
}
196+
}
197+
198+
init(
199+
name:String,
200+
client:Binding<Client>,
201+
absolute_path:String,
202+
path:[String],
203+
dir:Bool=false,
204+
contents:[FilePickerItemModel]?=nil
205+
){
206+
self.name= name
207+
_client= client
208+
self.path= path
209+
self.dir= dir
210+
self.absolute_path= absolute_path
211+
self.contents= contents
212+
213+
// Swift Arrays are COW
214+
id= path
215+
}
216+
217+
func loadContents(){
218+
self.error=nil
219+
withAnimation{ isLoading=true}
220+
Task{
221+
defer{
222+
withAnimation{
223+
isLoading=false
224+
innerIsExpanded=true
225+
}
226+
}
227+
dothrows(ClientError){
228+
contents=tryawait client
229+
.listAgentDirectory(.init(path: path, relativity:.root))
230+
.toModels(client: $client, path: path)
231+
} catch{
232+
self.error= error
233+
}
234+
}
235+
}
236+
}
237+
238+
extensionLSResponse{
239+
@MainActor
240+
func toModels(client:Binding<Client>, path:[String])->[FilePickerItemModel]{
241+
contents.compactMap{ filein
242+
// Filter dotfiles from the picker
243+
guard !file.name.hasPrefix(".")else{returnnil}
244+
245+
returnFilePickerItemModel(
246+
name: file.name,
247+
client: client,
248+
absolute_path: file.absolute_path_string,
249+
path: path+[file.name],
250+
dir: file.is_dir,
251+
contents:nil
252+
)
253+
}
254+
}
255+
}

‎Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
1313

1414
@Stateprivatevarloading:Bool=false
1515
@StateprivatevarcreateError:DaemonError?
16+
@StateprivatevarpickingRemote:Bool=false
1617

1718
varbody:someView{
1819
letagents= vpn.menuState.onlineAgents
@@ -46,7 +47,16 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
4647
}
4748
}
4849
Section{
49-
TextField("Remote Path", text: $remotePath)
50+
HStack(spacing:5){
51+
TextField("Remote Path", text: $remotePath)
52+
Spacer()
53+
Button{
54+
pickingRemote=true
55+
} label:{
56+
Image(systemName:"folder")
57+
}.disabled(workspace==nil)
58+
.help(workspace==nil?"Select a workspace first":"Open File Picker")
59+
}
5060
}
5161
}.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
5262
Divider()
@@ -71,6 +81,9 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
7181
set:{if !$0{ createError=nil}}
7282
)){} message:{
7383
Text(createError?.description??"An unknown error occurred.")
84+
}.sheet(isPresented: $pickingRemote){
85+
FilePicker(host: workspace!.primaryHost!, outputAbsPath: $remotePath)
86+
.frame(width:300, height:400)
7487
}
7588
}
7689

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp