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

Commit93a8e1c

Browse files
committed
feat: add stubbed file sync UI
1 parent4fb7970 commit93a8e1c

19 files changed

+414
-46
lines changed

‎Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ struct DesktopApp: App {
2323
.environmentObject(appDelegate.state)
2424
}
2525
.windowResizability(.contentSize)
26+
Window("File Sync", id:Windows.fileSync.rawValue){
27+
FileSyncConfig<CoderVPNService,MutagenDaemon>()
28+
.environmentObject(appDelegate.state)
29+
.environmentObject(appDelegate.fileSyncDaemon)
30+
.environmentObject(appDelegate.vpn)
31+
}
2632
}
2733
}
2834

@@ -41,9 +47,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4147
}
4248
vpn.installSystemExtension()
4349
#if arch(arm64)
44-
letmutagenBinary="mutagen-darwin-arm64"
50+
letmutagenBinary="mutagen-darwin-arm64"
4551
#elseif arch(x86_64)
46-
letmutagenBinary="mutagen-darwin-amd64"
52+
letmutagenBinary="mutagen-darwin-amd64"
4753
#endif
4854
fileSyncDaemon=MutagenDaemon(
4955
mutagenPath:Bundle.main.url(forResource: mutagenBinary, withExtension:nil)
@@ -61,9 +67,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
6167
awaitself.state.handleTokenExpiry()
6268
}
6369
}, content:{
64-
VPNMenu<CoderVPNService>().frame(width:256)
70+
VPNMenu<CoderVPNService,MutagenDaemon>().frame(width:256)
6571
.environmentObject(self.vpn)
6672
.environmentObject(self.state)
73+
.environmentObject(self.fileSyncDaemon)
6774
}
6875
))
6976
// Subscribe to system VPN updates
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import VPNLib
2+
3+
@MainActor
4+
finalclassPreviewFileSync:FileSyncDaemon{
5+
varsessionState:[VPNLib.FileSyncSession]=[]
6+
7+
varstate:DaemonState=.running
8+
9+
init(){}
10+
11+
func refreshSessions()async{}
12+
13+
func start()asyncthrows(DaemonError){
14+
state=.running
15+
}
16+
17+
func stop()async{
18+
state=.stopped
19+
}
20+
21+
func createSession(localPath _:String, agentHost _:String, remotePath _:String)asyncthrows(DaemonError){}
22+
23+
func deleteSessions(ids _:[String])asyncthrows(VPNLib.DaemonError){}
24+
}

‎Coder-Desktop/Coder-Desktop/VPN/MenuState.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import SwiftUI
33
import VPNLib
44

5-
structAgent:Identifiable,Equatable,Comparable{
5+
structAgent:Identifiable,Equatable,Comparable,Hashable{
66
letid:UUID
77
letname:String
88
letstatus:AgentStatus
@@ -135,6 +135,10 @@ struct VPNMenuState {
135135
return items.sorted()
136136
}
137137

138+
varonlineAgents:[Agent]{
139+
agents.map(\.value).filter{ $0.primaryHost!=nil}
140+
}
141+
138142
mutatingfunc clear(){
139143
agents.removeAll()
140144
workspaces.removeAll()
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import SwiftUI
2+
import VPNLib
3+
4+
structFileSyncConfig<VPN:VPNService, FS:FileSyncDaemon>:View{
5+
@EnvironmentObjectvarvpn:VPN
6+
@EnvironmentObjectvarfileSync:FS
7+
8+
@Stateprivatevarselection:FileSyncSession.ID?
9+
@StateprivatevaraddingNewSession:Bool=false
10+
@StateprivatevareditingSession:FileSyncSession?
11+
12+
@Stateprivatevarloading:Bool=false
13+
@StateprivatevardeleteError:DaemonError?
14+
15+
varbody:someView{
16+
Group{
17+
Table(fileSync.sessionState, selection: $selection){
18+
TableColumn("Local Path"){
19+
Text($0.alphaPath).help($0.alphaPath)
20+
}.width(min:200, ideal:240)
21+
TableColumn("Workspace", value: \.agentHost)
22+
.width(min:100, ideal:120)
23+
TableColumn("Remote Path", value: \.betaPath)
24+
.width(min:100, ideal:120)
25+
TableColumn("Status"){ $0.status.body}
26+
.width(min:80, ideal:100)
27+
TableColumn("Size"){ itemin
28+
Text(item.size)
29+
}
30+
.width(min:60, ideal:80)
31+
}
32+
.frame(minWidth:400, minHeight:200)
33+
.padding(.bottom,25)
34+
.overlay(alignment:.bottom){
35+
VStack(alignment:.leading, spacing:0){
36+
Divider()
37+
HStack(spacing:0){
38+
Button{
39+
addingNewSession=true
40+
} label:{
41+
Image(systemName:"plus")
42+
.frame(width:24, height:24)
43+
}.disabled(vpn.menuState.agents.isEmpty)
44+
Divider()
45+
Button{
46+
Task{
47+
loading=true
48+
defer{ loading=false}
49+
dothrows(DaemonError){
50+
tryawait fileSync.deleteSessions(ids:[selection!])
51+
} catch{
52+
deleteError= error
53+
}
54+
await fileSync.refreshSessions()
55+
selection=nil
56+
}
57+
} label:{
58+
Image(systemName:"minus").frame(width:24, height:24)
59+
}.disabled(selection==nil)
60+
iflet selection{
61+
iflet selectedSession= fileSync.sessionState.first(where:{ $0.id== selection}){
62+
Divider()
63+
Button{
64+
// TODO: Pause & Unpause
65+
} label:{
66+
switch selectedSession.status{
67+
case.paused:
68+
Image(systemName:"play").frame(width:24, height:24)
69+
default:
70+
Image(systemName:"pause").frame(width:24, height:24)
71+
}
72+
}
73+
}
74+
}
75+
}
76+
.buttonStyle(.borderless)
77+
}
78+
.background(.primary.opacity(0.04))
79+
.fixedSize(horizontal:false, vertical:true)
80+
}
81+
}.sheet(isPresented: $addingNewSession){
82+
FileSyncSessionModal<VPN,FS>()
83+
.frame(width:700)
84+
}.sheet(item: $editingSession){ sessionin
85+
FileSyncSessionModal<VPN,FS>(existingSession: session)
86+
.frame(width:700)
87+
}.alert("Error", isPresented:Binding(
88+
get:{ deleteError!=nil},
89+
set:{ isPresentedin
90+
if !isPresented{
91+
deleteError=nil
92+
}
93+
}
94+
)){} message:{
95+
Text(deleteError?.description??"An unknown error occurred. This should never happen.")
96+
}.task{
97+
while !Task.isCancelled{
98+
await fileSync.refreshSessions()
99+
try?awaitTask.sleep(for:.seconds(2))
100+
}
101+
}.disabled(loading)
102+
}
103+
}
104+
105+
#if DEBUG
106+
#Preview{
107+
FileSyncConfig<PreviewVPN,PreviewFileSync>()
108+
.environmentObject(AppState(persistent:false))
109+
.environmentObject(PreviewVPN())
110+
.environmentObject(PreviewFileSync())
111+
}
112+
#endif
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import SwiftUI
2+
import VPNLib
3+
4+
structFileSyncSessionModal<VPN:VPNService, FS:FileSyncDaemon>:View{
5+
varexistingSession:FileSyncSession?
6+
@Environment(\.dismiss)privatevardismiss
7+
@EnvironmentObjectprivatevarvpn:VPN
8+
@EnvironmentObjectprivatevarfileSync:FS
9+
10+
@StateprivatevarlocalPath:String=""
11+
@Stateprivatevarworkspace:Agent?
12+
@StateprivatevarremotePath:String=""
13+
14+
@Stateprivatevarloading:Bool=false
15+
@StateprivatevarcreateError:DaemonError?
16+
17+
varbody:someView{
18+
letagents= vpn.menuState.onlineAgents
19+
VStack(spacing:0){
20+
Form{
21+
Section{
22+
HStack(spacing:5){
23+
TextField("Local Path", text: $localPath)
24+
Spacer()
25+
Button{
26+
letpanel=NSOpenPanel()
27+
panel.directoryURL=FileManager.default.homeDirectoryForCurrentUser
28+
panel.allowsMultipleSelection=false
29+
panel.canChooseDirectories=true
30+
panel.canChooseFiles=false
31+
if panel.runModal()==.OK{
32+
localPath= panel.url?.path(percentEncoded:false)??"<none>"
33+
}
34+
} label:{
35+
Image(systemName:"folder")
36+
}
37+
}
38+
}
39+
Section{
40+
Picker("Workspace", selection: $workspace){
41+
ForEach(agents, id: \.id){ agentin
42+
Text(agent.primaryHost!).tag(agent)
43+
}
44+
// HACK: Silence error logs for no-selection.
45+
Divider().tag(nilasAgent?)
46+
}
47+
}
48+
Section{
49+
TextField("Remote Path", text: $remotePath)
50+
}
51+
}.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
52+
Divider()
53+
HStack{
54+
Spacer()
55+
if loading{
56+
ProgressView()
57+
}
58+
Button("Cancel", action:{dismiss()}).keyboardShortcut(.cancelAction)
59+
Button(existingSession==nil?"Add":"Save"){Task{awaitsubmit()}}
60+
.keyboardShortcut(.defaultAction)
61+
}.padding(20)
62+
}.onAppear{
63+
iflet existingSession{
64+
localPath= existingSession.alphaPath
65+
workspace= agents.first{ $0.primaryHost== existingSession.agentHost}
66+
remotePath= existingSession.betaPath
67+
}else{
68+
// Set the picker to the first agent by default
69+
workspace= agents.first
70+
}
71+
}.disabled(loading)
72+
.alert("Error", isPresented:Binding(
73+
get:{ createError!=nil},
74+
set:{if $0{ createError=nil}}
75+
)){} message:{
76+
Text(createError?.description??"An unknown error occurred. This should never happen.")
77+
}
78+
}
79+
80+
func submit()async{
81+
createError=nil
82+
guardlet workspaceelse{
83+
return
84+
}
85+
loading=true
86+
defer{ loading=false}
87+
dothrows(DaemonError){
88+
iflet existingSession{
89+
// TODO: Support selecting & deleting multiple sessions at once
90+
tryawait fileSync.deleteSessions(ids:[existingSession.id])
91+
}
92+
tryawait fileSync.createSession(
93+
localPath: localPath,
94+
agentHost: workspace.primaryHost!,
95+
remotePath: remotePath
96+
)
97+
} catch{
98+
createError= error
99+
return
100+
}
101+
dismiss()
102+
}
103+
}

‎Coder-Desktop/Coder-Desktop/Views/LoginForm.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,8 @@ struct LoginForm: View {
4848
loginError=nil
4949
}
5050
}
51-
)){
52-
Button("OK", role:.cancel){}.keyboardShortcut(.defaultAction)
53-
} message:{
54-
Text(loginError?.description??"")
51+
)){} message:{
52+
Text(loginError?.description??"An unknown error occurred. This should never happen.")
5553
}.disabled(loading)
5654
.frame(width:550)
5755
.fixedSize()

‎Coder-Desktop/Coder-Desktop/Views/Settings/LiteralHeadersSection.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct LiteralHeadersSection<VPN: VPNService>: View {
1515
Toggle(isOn: $state.useLiteralHeaders){
1616
Text("HTTP Headers")
1717
Text("When enabled, these headers will be included on all outgoing HTTP requests.")
18-
if vpn.state!=.disabled{Text("Cannot be modified while Coder Connect is enabled.")}
18+
if!vpn.state.canBeStarted{Text("Cannot be modified while Coder Connect is enabled.")}
1919
}
2020
.controlSize(.large)
2121

@@ -65,7 +65,7 @@ struct LiteralHeadersSection<VPN: VPNService>: View {
6565
LiteralHeaderModal(existingHeader: header)
6666
}.onTapGesture{
6767
selectedHeader=nil
68-
}.disabled(vpn.state!=.disabled)
68+
}.disabled(!vpn.state.canBeStarted)
6969
.onReceive(inspection.notice){ inspection.visit(self, $0)} // ViewInspector
7070
}
7171
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import SwiftUI
2+
3+
structStatusDot:View{
4+
letcolor:Color
5+
6+
varbody:someView{
7+
ZStack{
8+
Circle()
9+
.fill(color.opacity(0.4))
10+
.frame(width:12, height:12)
11+
Circle()
12+
.fill(color.opacity(1.0))
13+
.frame(width:7, height:7)
14+
}
15+
}
16+
}

‎Coder-Desktop/Coder-Desktop/Views/VPNMenu.swiftrenamed to‎Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import SwiftUI
2+
import VPNLib
23

3-
structVPNMenu<VPN:VPNService>:View{
4+
structVPNMenu<VPN:VPNService, FS:FileSyncDaemon>:View{
45
@EnvironmentObjectvarvpn:VPN
6+
@EnvironmentObjectvarfileSync:FS
57
@EnvironmentObjectvarstate:AppState
68
@Environment(\.openSettings)privatevaropenSettings
79
@Environment(\.openWindow)privatevaropenWindow
@@ -60,6 +62,24 @@ struct VPNMenu<VPN: VPNService>: View {
6062
}.buttonStyle(.plain)
6163
TrayDivider()
6264
}
65+
if vpn.state==.connected{
66+
Button{
67+
openWindow(id:.fileSync)
68+
} label:{
69+
ButtonRowView{
70+
HStack{
71+
// TODO: A future PR will provide users a way to recover from a daemon failure without
72+
// needing to restart the app
73+
if case.failed= fileSync.state,sessionsHaveError(fileSync.sessionState){
74+
Image(systemName:"exclamationmark.arrow.trianglehead.2.clockwise.rotate.90")
75+
.frame(width:12, height:12).help("One or more sync sessions have errors")
76+
}
77+
Text("File sync")
78+
}
79+
}
80+
}.buttonStyle(.plain)
81+
TrayDivider()
82+
}
6383
if vpn.state==.failed(.systemExtensionError(.needsUserApproval)){
6484
Button{
6585
openSystemExtensionSettings()
@@ -119,8 +139,9 @@ func openSystemExtensionSettings() {
119139
appState.login(baseAccessURL:URL(string:"http://127.0.0.1:8080")!, sessionToken:"")
120140
// appState.clearSession()
121141

122-
returnVPNMenu<PreviewVPN>().frame(width:256)
142+
returnVPNMenu<PreviewVPN,PreviewFileSync>().frame(width:256)
123143
.environmentObject(PreviewVPN())
124144
.environmentObject(appState)
145+
.environmentObject(PreviewFileSync())
125146
}
126147
#endif

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp