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

Commitfe20801

Browse files
feat: add conflict descriptions and file sync context menu (#126)
Last QoL PR for now..This adds buttons to the alt click context menu:<img width="971" alt="Screenshot 2025-03-28 at 3 25 12 pm" src="https://github.com/user-attachments/assets/2477ee7d-2466-4fa5-9f7f-53a711ffdd64" />And it adds a brief description of each conflict to the status tooltip:<img width="405" alt="image" src="https://github.com/user-attachments/assets/e513faf1-414f-4612-902d-204b277a34b1" />There's three cases for now. The first is just a basic file conflict, the second is if there's a type conflict (file, directory, symlink, etc), and the third is self-explanatory.We'll need to come up with a proper design for how we show conflicts, so this implementation is just to not leave users in the dark if they run into any.
1 parent1fd5855 commitfe20801

File tree

3 files changed

+183
-26
lines changed

3 files changed

+183
-26
lines changed

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

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,23 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
2929
TableColumn("Size"){Text($0.localSize.humanSizeBytes).help($0.sizeDescription)}
3030
.width(min:60, ideal:80)
3131
}
32-
.contextMenu(forSelectionType:FileSyncSession.ID.self, menu:{ _in},
33-
primaryAction:{ selectedSessionsin
34-
iflet session= selectedSessions.first{
35-
editingSession= fileSync.sessionState.first(where:{ $0.id== session})
36-
}
37-
})
32+
.contextMenu(forSelectionType:FileSyncSession.ID.self, menu:{ selectionsin
33+
// TODO: We only support single selections for now
34+
iflet selected= selections.first,
35+
let session= fileSync.sessionState.first(where:{ $0.id== selected})
36+
{
37+
Button("Edit"){ editingSession= session}
38+
Button(session.status.isResumable?"Resume":"Pause")
39+
{Task{awaitpauseResume(session: session)}}
40+
Button("Reset"){Task{awaitreset(session: session)}}
41+
Button("Terminate"){Task{awaitdelete(session: session)}}
42+
}
43+
},
44+
primaryAction:{ selectedSessionsin
45+
iflet session= selectedSessions.first{
46+
editingSession= fileSync.sessionState.first(where:{ $0.id== session})
47+
}
48+
})
3849
.frame(minWidth:400, minHeight:200)
3950
.padding(.bottom,25)
4051
.overlay(alignment:.bottom){
@@ -142,12 +153,9 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
142153
Divider()
143154
Button{Task{awaitpauseResume(session: selectedSession)}}
144155
label:{
145-
switch selectedSession.status{
146-
case.paused,.error(.haltedOnRootEmptied),
147-
.error(.haltedOnRootDeletion),
148-
.error(.haltedOnRootTypeChange):
156+
if selectedSession.status.isResumable{
149157
Image(systemName:"play").frame(width:24, height:24).help("Pause")
150-
default:
158+
}else{
151159
Image(systemName:"pause").frame(width:24, height:24).help("Resume")
152160
}
153161
}
@@ -182,12 +190,9 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
182190
loading=true
183191
defer{ loading=false}
184192
dothrows(DaemonError){
185-
switch session.status{
186-
case.paused,.error(.haltedOnRootEmptied),
187-
.error(.haltedOnRootDeletion),
188-
.error(.haltedOnRootTypeChange):
193+
if session.status.isResumable{
189194
tryawait fileSync.resumeSessions(ids:[session.id])
190-
default:
195+
}else{
191196
tryawait fileSync.pauseSessions(ids:[session.id])
192197
}
193198
} catch{

‎Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ public struct FileSyncSession: Identifiable {
4646
}
4747
if case.error= status{}else{
4848
if state.conflicts.count>0{
49-
status=.conflicts
49+
status=.conflicts(
50+
formatConflicts(
51+
conflicts: state.conflicts,
52+
excludedConflicts: state.excludedConflicts
53+
)
54+
)
5055
}
5156
}
5257
self.status= status
@@ -121,7 +126,7 @@ public enum FileSyncStatus {
121126
case error(FileSyncErrorStatus)
122127
case ok
123128
case paused
124-
case conflicts
129+
case conflicts(String)
125130
case working(FileSyncWorkingStatus)
126131

127132
publicvarcolor:Color{
@@ -168,8 +173,8 @@ public enum FileSyncStatus {
168173
"The session is watching for filesystem changes."
169174
case.paused:
170175
"The session is paused."
171-
case.conflicts:
172-
"The session has conflicts that need to be resolved."
176+
caselet.conflicts(details):
177+
"The session has conflicts that need to be resolved:\n\n\(details)"
173178
caselet.working(status):
174179
status.description
175180
}
@@ -178,6 +183,18 @@ public enum FileSyncStatus {
178183
publicvarcolumn:someView{
179184
Text(type).foregroundColor(color)
180185
}
186+
187+
publicvarisResumable:Bool{
188+
switchself{
189+
case.paused,
190+
.error(.haltedOnRootEmptied),
191+
.error(.haltedOnRootDeletion),
192+
.error(.haltedOnRootTypeChange):
193+
true
194+
default:
195+
false
196+
}
197+
}
181198
}
182199

183200
publicenumFileSyncWorkingStatus{
@@ -272,8 +289,8 @@ public enum FileSyncErrorStatus {
272289
}
273290

274291
publicenumFileSyncEndpoint{
275-
caselocal
276-
caseremote
292+
casealpha
293+
casebeta
277294
}
278295

279296
publicenumFileSyncProblemType{
@@ -284,13 +301,16 @@ public enum FileSyncProblemType {
284301
publicenumFileSyncError{
285302
case generic(String)
286303
case problem(FileSyncEndpoint,FileSyncProblemType, path:String, error:String)
304+
case excludedProblems(FileSyncEndpoint,FileSyncProblemType,UInt64)
287305

288306
vardescription:String{
289307
switchself{
290308
caselet.generic(error):
291309
error
292310
caselet.problem(endpoint, type, path, error):
293311
"\(endpoint)\(type) error at\(path):\(error)"
312+
caselet.excludedProblems(endpoint, type, count):
313+
"+\(count)\(endpoint)\(type) problems"
294314
}
295315
}
296316
}

‎Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,28 @@ func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] {
4040
errors.append(.generic(state.lastError))
4141
}
4242
forproblemin state.alphaState.scanProblems{
43-
errors.append(.problem(.local,.scan, path: problem.path, error: problem.error))
43+
errors.append(.problem(.alpha,.scan, path: problem.path, error: problem.error))
4444
}
4545
forproblemin state.alphaState.transitionProblems{
46-
errors.append(.problem(.local,.transition, path: problem.path, error: problem.error))
46+
errors.append(.problem(.alpha,.transition, path: problem.path, error: problem.error))
4747
}
4848
forproblemin state.betaState.scanProblems{
49-
errors.append(.problem(.remote,.scan, path: problem.path, error: problem.error))
49+
errors.append(.problem(.beta,.scan, path: problem.path, error: problem.error))
5050
}
5151
forproblemin state.betaState.transitionProblems{
52-
errors.append(.problem(.remote,.transition, path: problem.path, error: problem.error))
52+
errors.append(.problem(.beta,.transition, path: problem.path, error: problem.error))
53+
}
54+
if state.alphaState.excludedScanProblems>0{
55+
errors.append(.excludedProblems(.alpha,.scan, state.alphaState.excludedScanProblems))
56+
}
57+
if state.alphaState.excludedTransitionProblems>0{
58+
errors.append(.excludedProblems(.alpha,.transition, state.alphaState.excludedTransitionProblems))
59+
}
60+
if state.betaState.excludedScanProblems>0{
61+
errors.append(.excludedProblems(.beta,.scan, state.betaState.excludedScanProblems))
62+
}
63+
if state.betaState.excludedTransitionProblems>0{
64+
errors.append(.excludedProblems(.beta,.transition, state.betaState.excludedTransitionProblems))
5365
}
5466
return errors
5567
}
@@ -80,3 +92,123 @@ extension Prompting_HostResponse {
8092
}
8193
}
8294
}
95+
96+
// Translated from `cmd/mutagen/sync/list_monitor_common.go`
97+
func formatConflicts(conflicts:[Core_Conflict], excludedConflicts:UInt64)->String{
98+
varresult=""
99+
for(i, conflict)in conflicts.enumerated(){
100+
varchangesByPath:[String:(alpha:[Core_Change], beta:[Core_Change])]=[:]
101+
102+
// Group alpha changes by path
103+
foralphaChangein conflict.alphaChanges{
104+
letpath= alphaChange.path
105+
ifchangesByPath[path]==nil{
106+
changesByPath[path]=(alpha:[], beta:[])
107+
}
108+
changesByPath[path]!.alpha.append(alphaChange)
109+
}
110+
111+
// Group beta changes by path
112+
forbetaChangein conflict.betaChanges{
113+
letpath= betaChange.path
114+
ifchangesByPath[path]==nil{
115+
changesByPath[path]=(alpha:[], beta:[])
116+
}
117+
changesByPath[path]!.beta.append(betaChange)
118+
}
119+
120+
result+=formatChanges(changesByPath)
121+
122+
if i< conflicts.count-1 || excludedConflicts>0{
123+
result+="\n"
124+
}
125+
}
126+
127+
if excludedConflicts>0{
128+
result+="...+\(excludedConflicts) more conflicts...\n"
129+
}
130+
131+
return result
132+
}
133+
134+
func formatChanges(_ changesByPath:[String:(alpha:[Core_Change], beta:[Core_Change])])->String{
135+
varresult=""
136+
137+
for(path, changes)in changesByPath{
138+
if changes.alpha.count==1, changes.beta.count==1{
139+
// Simple message for basic file conflicts
140+
if changes.alpha[0].hasNew,
141+
changes.beta[0].hasNew,
142+
changes.alpha[0].new.kind==.file,
143+
changes.beta[0].new.kind==.file
144+
{
145+
result+="File: '\(formatPath(path))'\n"
146+
continue
147+
}
148+
// Friendly message for `<non-existent -> !<non-existent>` conflicts
149+
if !changes.alpha[0].hasOld,
150+
!changes.beta[0].hasOld,
151+
changes.alpha[0].hasNew,
152+
changes.beta[0].hasNew
153+
{
154+
result+="""
155+
An entry, '\(formatPath(path))', was created on both endpoints that does not match.
156+
You can resolve this conflict by deleting one of the entries.\n
157+
"""
158+
continue
159+
}
160+
}
161+
162+
letformattedPath=formatPath(path)
163+
result+="Path: '\(formattedPath)'\n"
164+
165+
// TODO: Local & Remote should be replaced with Alpha & Beta, once it's possible to configure which is which
166+
167+
if !changes.alpha.isEmpty{
168+
result+=" Local changes:\n"
169+
forchangein changes.alpha{
170+
letold=formatEntry(change.hasOld? change.old:nil)
171+
letnew=formatEntry(change.hasNew? change.new:nil)
172+
result+="\(old)\(new)\n"
173+
}
174+
}
175+
176+
if !changes.beta.isEmpty{
177+
result+=" Remote changes:\n"
178+
forchangein changes.beta{
179+
letold=formatEntry(change.hasOld? change.old:nil)
180+
letnew=formatEntry(change.hasNew? change.new:nil)
181+
result+="\(old)\(new)\n"
182+
}
183+
}
184+
}
185+
186+
return result
187+
}
188+
189+
func formatPath(_ path:String)->String{
190+
path.isEmpty?"<root>": path
191+
}
192+
193+
func formatEntry(_ entry:Core_Entry?)->String{
194+
guardlet entryelse{
195+
return"<non-existent>"
196+
}
197+
198+
switch entry.kind{
199+
case.directory:
200+
return"Directory"
201+
case.file:
202+
return entry.executable?"Executable File":"File"
203+
case.symbolicLink:
204+
return"Symbolic Link (\(entry.target))"
205+
case.untracked:
206+
return"Untracked content"
207+
case.problematic:
208+
return"Problematic content (\(entry.problem))"
209+
case.UNRECOGNIZED:
210+
return"<unknown>"
211+
case.phantomDirectory:
212+
return"Phantom Directory"
213+
}
214+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp