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

Commit25ad797

Browse files
1 parent6210775 commit25ad797

File tree

5 files changed

+153
-45
lines changed

5 files changed

+153
-45
lines changed

‎Coder-Desktop/Coder-Desktop/Theme.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@ enum Theme {
1313
staticletappIconSize:CGSize=.init(width: appIconWidth, height: appIconHeight)
1414
}
1515

16+
enumAnimation{
17+
staticletcollapsibleDuration=0.2
18+
}
19+
1620
staticletdefaultVisibleAgents=5
1721
}

‎Coder-Desktop/Coder-Desktop/Views/VPN/Agents.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ struct Agents<VPN: VPNService>: View {
44
@EnvironmentObjectvarvpn:VPN
55
@EnvironmentObjectvarstate:AppState
66
@StateprivatevarviewAll=false
7+
@StateprivatevarexpandedItem:VPNMenuItem.ID?
8+
@StateprivatevarhasToggledExpansion:Bool=false
79
privateletdefaultVisibleRows=5
810

911
letinspection=Inspection<Self>()
@@ -15,8 +17,24 @@ struct Agents<VPN: VPNService>: View {
1517
letitems= vpn.menuState.sorted
1618
letvisibleItems= viewAll?items[...]: items.prefix(defaultVisibleRows)
1719
ForEach(visibleItems, id: \.id){ agentin
18-
MenuItemView(item: agent, baseAccessURL: state.baseAccessURL!)
19-
.padding(.horizontal,Theme.Size.trayMargin)
20+
MenuItemView(
21+
item: agent,
22+
baseAccessURL: state.baseAccessURL!,
23+
expandedItem: $expandedItem,
24+
userInteracted: $hasToggledExpansion
25+
)
26+
.padding(.horizontal,Theme.Size.trayMargin)
27+
}.onChange(of: visibleItems){
28+
// If no workspaces are online, we should expand the first one to come online
29+
if visibleItems.filter({ $0.status!=.off}).isEmpty{
30+
hasToggledExpansion=false
31+
return
32+
}
33+
if hasToggledExpansion{
34+
return
35+
}
36+
expandedItem= visibleItems.first?.id
37+
hasToggledExpansion=true
2038
}
2139
if items.count==0{
2240
Text("No workspaces!")

‎Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift

Lines changed: 126 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
3535
}
3636
}
3737

38+
func primaryHost(hostnameSuffix:String)->String{
39+
switchself{
40+
caselet.agent(agent): agent.primaryHost
41+
case.offlineWorkspace:"\(wsName).\(hostnameSuffix)"
42+
}
43+
}
44+
3845
staticfunc<(lhs:VPNMenuItem, rhs:VPNMenuItem)->Bool{
3946
switch(lhs, rhs){
4047
caselet(.agent(lhsAgent),.agent(rhsAgent)):
@@ -52,23 +59,23 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
5259

5360
structMenuItemView:View{
5461
@EnvironmentObjectvarstate:AppState
62+
@Environment(\.openURL)privatevaropenURL
5563

5664
privateletlogger=Logger(subsystem:Bundle.main.bundleIdentifier!, category:"VPNMenu")
5765

5866
letitem:VPNMenuItem
5967
letbaseAccessURL:URL
68+
@BindingvarexpandedItem:VPNMenuItem.ID?
69+
@BindingvaruserInteracted:Bool
6070

6171
@StateprivatevarnameIsSelected:Bool=false
62-
@StateprivatevarcopyIsSelected:Bool=false
6372

64-
privateletdefaultVisibleApps=5
6573
@Stateprivatevarapps:[WorkspaceApp]=[]
6674

75+
varhasApps:Bool{ !apps.isEmpty}
76+
6777
privatevaritemName:AttributedString{
68-
letname=switch item{
69-
caselet.agent(agent): agent.primaryHost
70-
case.offlineWorkspace:"\(item.wsName).\(state.hostnameSuffix)"
71-
}
78+
letname= item.primaryHost(hostnameSuffix: state.hostnameSuffix)
7279

7380
varformattedName=AttributedString(name)
7481
formattedName.foregroundColor=.primary
@@ -79,17 +86,34 @@ struct MenuItemView: View {
7986
return formattedName
8087
}
8188

89+
privatevarisExpanded:Bool{
90+
expandedItem== item.id
91+
}
92+
8293
privatevarwsURL:URL{
8394
// TODO: CoderVPN currently only supports owned workspaces
8495
baseAccessURL.appending(path:"@me").appending(path: item.wsName)
8596
}
8697

98+
privatefunc toggleExpanded(){
99+
userInteracted=true
100+
if isExpanded{
101+
withAnimation(.snappy(duration:Theme.Animation.collapsibleDuration)){
102+
expandedItem=nil
103+
}
104+
}else{
105+
withAnimation(.snappy(duration:Theme.Animation.collapsibleDuration)){
106+
expandedItem= item.id
107+
}
108+
}
109+
}
110+
87111
varbody:someView{
88112
VStack(spacing:0){
89-
HStack(spacing:0){
90-
Link(destination: wsURL){
113+
HStack(spacing:3){
114+
Button(action: toggleExpanded){
91115
HStack(spacing:Theme.Size.trayPadding){
92-
StatusDot(color:item.status.color)
116+
AnimatedChevron(isExpanded: isExpanded,color:.secondary)
93117
Text(itemName).lineLimit(1).truncationMode(.tail)
94118
Spacer()
95119
}.padding(.horizontal,Theme.Size.trayPadding)
@@ -98,42 +122,24 @@ struct MenuItemView: View {
98122
.foregroundStyle(nameIsSelected?.white:.primary)
99123
.background(nameIsSelected?Color.accentColor.opacity(0.8):.clear)
100124
.clipShape(.rect(cornerRadius:Theme.Size.rectCornerRadius))
101-
.onHoverWithPointingHand{ hoveringin
125+
.onHover{ hoveringin
102126
nameIsSelected= hovering
103127
}
104-
Spacer()
105-
}.buttonStyle(.plain)
106-
if caselet.agent(agent)= item{
107-
Button{
108-
NSPasteboard.general.clearContents()
109-
NSPasteboard.general.setString(agent.primaryHost, forType:.string)
110-
} label:{
111-
Image(systemName:"doc.on.doc")
112-
.symbolVariant(.fill)
113-
.padding(3)
114-
.contentShape(Rectangle())
115-
}.foregroundStyle(copyIsSelected?.white:.primary)
116-
.imageScale(.small)
117-
.background(copyIsSelected?Color.accentColor.opacity(0.8):.clear)
118-
.clipShape(.rect(cornerRadius:Theme.Size.rectCornerRadius))
119-
.onHoverWithPointingHand{ hoveringin copyIsSelected= hovering}
120-
.buttonStyle(.plain)
121-
.padding(.trailing,Theme.Size.trayMargin)
122-
}
128+
}.buttonStyle(.plain).padding(.trailing,3)
129+
MenuItemIcons(item: item, wsURL: wsURL)
123130
}
124-
if !apps.isEmpty{
125-
HStack(spacing:17){
126-
ForEach(apps.prefix(defaultVisibleApps), id: \.id){ appin
127-
WorkspaceAppIcon(app: app)
128-
.frame(width:Theme.Size.appIconWidth, height:Theme.Size.appIconHeight)
129-
}
130-
if apps.count< defaultVisibleApps{
131-
Spacer()
131+
if isExpanded{
132+
if hasApps{
133+
MenuItemCollapsibleView(apps: apps)
134+
}else{
135+
HStack{
136+
Text(item.status==.off?"Workspace is offline.":"No apps available.")
137+
.font(.body)
138+
.foregroundColor(.secondary)
139+
.padding(.horizontal,Theme.Size.trayInset)
140+
.padding(.top,7)
132141
}
133142
}
134-
.padding(.leading, apps.count< defaultVisibleApps?14:0)
135-
.padding(.bottom,5)
136-
.padding(.top,10)
137143
}
138144
}
139145
.task{awaitloadApps()}
@@ -172,3 +178,83 @@ struct MenuItemView: View {
172178
}
173179
}
174180
}
181+
182+
structMenuItemCollapsibleView:View{
183+
privateletdefaultVisibleApps=5
184+
letapps:[WorkspaceApp]
185+
186+
varbody:someView{
187+
HStack(spacing:17){
188+
ForEach(apps.prefix(defaultVisibleApps), id: \.id){ appin
189+
WorkspaceAppIcon(app: app)
190+
.frame(width:Theme.Size.appIconWidth, height:Theme.Size.appIconHeight)
191+
}
192+
if apps.count< defaultVisibleApps{
193+
Spacer()
194+
}
195+
}
196+
.padding(.leading, apps.count< defaultVisibleApps?14:0)
197+
.padding(.bottom,5)
198+
.padding(.top,10)
199+
}
200+
}
201+
202+
structMenuItemIcons:View{
203+
@EnvironmentObjectvarstate:AppState
204+
@Environment(\.openURL)privatevaropenURL
205+
206+
letitem:VPNMenuItem
207+
letwsURL:URL
208+
209+
@StateprivatevarcopyIsSelected:Bool=false
210+
@StateprivatevarwebIsSelected:Bool=false
211+
212+
func copyToClipboard(){
213+
letprimaryHost= item.primaryHost(hostnameSuffix: state.hostnameSuffix)
214+
NSPasteboard.general.clearContents()
215+
NSPasteboard.general.setString(primaryHost, forType:.string)
216+
}
217+
218+
varbody:someView{
219+
StatusDot(color: item.status.color)
220+
.padding(.trailing,3)
221+
.padding(.top,1)
222+
MenuItemIconButton(systemName:"doc.on.doc", action: copyToClipboard)
223+
.font(.system(size:9))
224+
.symbolVariant(.fill)
225+
MenuItemIconButton(systemName:"globe", action:{openURL(wsURL)})
226+
.contentShape(Rectangle())
227+
.font(.system(size:12))
228+
.padding(.trailing,Theme.Size.trayMargin)
229+
}
230+
}
231+
232+
structMenuItemIconButton:View{
233+
letsystemName:String
234+
@StatevarisSelected:Bool=false
235+
letaction:@MainActor()->Void
236+
237+
varbody:someView{
238+
Button(action: action){
239+
Image(systemName: systemName)
240+
.padding(3)
241+
.contentShape(Rectangle())
242+
}.foregroundStyle(isSelected?.white:.primary)
243+
.background(isSelected?Color.accentColor.opacity(0.8):.clear)
244+
.clipShape(.rect(cornerRadius:Theme.Size.rectCornerRadius))
245+
.onHover{ hoveringin isSelected= hovering}
246+
.buttonStyle(.plain)
247+
}
248+
}
249+
250+
structAnimatedChevron:View{
251+
letisExpanded:Bool
252+
letcolor:Color
253+
254+
varbody:someView{
255+
Image(systemName:"chevron.right")
256+
.font(.system(size:12, weight:.semibold))
257+
.foregroundColor(color)
258+
.rotationEffect(.degrees(isExpanded?90:0))
259+
}
260+
}

‎Coder-Desktop/Coder-Desktop/Views/VPN/WorkspaceAppIcon.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ struct WorkspaceAppIcon: View {
3737
RoundedRectangle(cornerRadius:Theme.Size.rectCornerRadius*2)
3838
.stroke(.secondary, lineWidth:1)
3939
.opacity(isHovering && !isPressed?0.6:0.3)
40-
).onHoverWithPointingHand{ hoveringin isHovering= hovering}
40+
).onHover{ hoveringin isHovering= hovering}
4141
.simultaneousGesture(
4242
DragGesture(minimumDistance:0)
4343
.onChanged{ _in

‎Coder-Desktop/Coder-DesktopTests/AgentsTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ struct AgentsTests {
6262
letforEach=try view.inspect().find(ViewType.ForEach.self)
6363
#expect(forEach.count==Theme.defaultVisibleAgents)
6464
// Agents are sorted by status, and then by name in alphabetical order
65-
#expect(throws:Never.self){try view.inspect().find(link:"a1.coder")}
65+
#expect(throws:Never.self){try view.inspect().find(text:"a1.coder")}
6666
}
6767

6868
@Test
@@ -115,7 +115,7 @@ struct AgentsTests {
115115
tryawait sut.inspection.inspect{ viewin
116116
letforEach=try view.find(ViewType.ForEach.self)
117117
#expect(forEach.count==Theme.defaultVisibleAgents)
118-
#expect(throws:Never.self){try view.find(link:"offline.coder")}
118+
#expect(throws:Never.self){try view.find(text:"offline.coder")}
119119
}
120120
}
121121
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp