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

Commitc24fa6b

Browse files
committed
feat: make workspace apps collapsible
1 parent101baae commitc24fa6b

File tree

5 files changed

+149
-47
lines changed

5 files changed

+149
-47
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/VPN/MenuState.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
1010
letwsName:String
1111
letwsID:UUID
1212

13-
// Agents are sorted bystatus, and then byname
13+
// Agents are sorted by name
1414
staticfunc<(lhs:Agent, rhs:Agent)->Bool{
15-
if lhs.status!= rhs.status{
16-
return lhs.status< rhs.status
17-
}
18-
return lhs.wsName.localizedCompare(rhs.wsName)==.orderedAscending
15+
lhs.wsName.localizedCompare(rhs.wsName)==.orderedAscending
1916
}
2017

2118
letprimaryHost:String

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ struct Agents<VPN: VPNService>: View {
44
@EnvironmentObjectvarvpn:VPN
55
@EnvironmentObjectvarstate:AppState
66
@StateprivatevarviewAll=false
7+
@StateprivatevarexpandedItem:VPNMenuItem.ID?
78
privateletdefaultVisibleRows=5
89

910
letinspection=Inspection<Self>()
@@ -15,7 +16,7 @@ struct Agents<VPN: VPNService>: View {
1516
letitems= vpn.menuState.sorted
1617
letvisibleItems= viewAll?items[...]: items.prefix(defaultVisibleRows)
1718
ForEach(visibleItems, id: \.id){ agentin
18-
MenuItemView(item: agent, baseAccessURL: state.baseAccessURL!)
19+
MenuItemView(item: agent, baseAccessURL: state.baseAccessURL!, expandedItem: $expandedItem)
1920
.padding(.horizontal,Theme.Size.trayMargin)
2021
}
2122
if items.count==0{

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

Lines changed: 140 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,22 @@ 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?
6069

6170
@StateprivatevarnameIsSelected:Bool=false
62-
@StateprivatevarcopyIsSelected:Bool=false
6371

64-
privateletdefaultVisibleApps=5
6572
@Stateprivatevarapps:[WorkspaceApp]=[]
6673

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

7379
varformattedName=AttributedString(name)
7480
formattedName.foregroundColor=.primary
@@ -79,17 +85,33 @@ struct MenuItemView: View {
7985
return formattedName
8086
}
8187

88+
privatevarisExpanded:Bool{
89+
expandedItem== item.id
90+
}
91+
8292
privatevarwsURL:URL{
8393
// TODO: CoderVPN currently only supports owned workspaces
8494
baseAccessURL.appending(path:"@me").appending(path: item.wsName)
8595
}
8696

97+
privatefunc toggleExpanded(){
98+
if isExpanded{
99+
withAnimation(.snappy(duration:Theme.Animation.collapsibleDuration)){
100+
expandedItem=nil
101+
}
102+
}else{
103+
withAnimation(.snappy(duration:Theme.Animation.collapsibleDuration)){
104+
expandedItem= item.id
105+
}
106+
}
107+
}
108+
87109
varbody:someView{
88110
VStack(spacing:0){
89-
HStack(spacing:0){
90-
Link(destination: wsURL){
111+
HStack(spacing:3){
112+
Button(action: toggleExpanded){
91113
HStack(spacing:Theme.Size.trayPadding){
92-
StatusDot(color:item.status.color)
114+
AnimatedChevron(isExpanded: isExpanded,color:.secondary)
93115
Text(itemName).lineLimit(1).truncationMode(.tail)
94116
Spacer()
95117
}.padding(.horizontal,Theme.Size.trayPadding)
@@ -98,42 +120,24 @@ struct MenuItemView: View {
98120
.foregroundStyle(nameIsSelected?.white:.primary)
99121
.background(nameIsSelected?Color.accentColor.opacity(0.8):.clear)
100122
.clipShape(.rect(cornerRadius:Theme.Size.rectCornerRadius))
101-
.onHoverWithPointingHand{ hoveringin
123+
.onHover{ hoveringin
102124
nameIsSelected= hovering
103125
}
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-
}
126+
}.buttonStyle(.plain).padding(.trailing,3)
127+
MenuItemIcons(item: item, wsURL: wsURL)
123128
}
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()
129+
if isExpanded{
130+
if hasApps{
131+
MenuItemCollapsibleView(apps: apps)
132+
}else{
133+
HStack{
134+
Text(item.status==.off?"Workspace is offline.":"No apps available.")
135+
.font(.body)
136+
.foregroundColor(.secondary)
137+
.padding(.horizontal,Theme.Size.trayInset)
138+
.padding(.top,7)
132139
}
133140
}
134-
.padding(.leading, apps.count< defaultVisibleApps?14:0)
135-
.padding(.bottom,5)
136-
.padding(.top,10)
137141
}
138142
}
139143
.task{awaitloadApps()}
@@ -172,3 +176,99 @@ struct MenuItemView: View {
172176
}
173177
}
174178
}
179+
180+
structMenuItemCollapsibleView:View{
181+
privateletdefaultVisibleApps=5
182+
letapps:[WorkspaceApp]
183+
184+
varbody:someView{
185+
HStack(spacing:17){
186+
ForEach(apps.prefix(defaultVisibleApps), id: \.id){ appin
187+
WorkspaceAppIcon(app: app)
188+
.frame(width:Theme.Size.appIconWidth, height:Theme.Size.appIconHeight)
189+
}
190+
if apps.count< defaultVisibleApps{
191+
Spacer()
192+
}
193+
}
194+
.padding(.leading, apps.count< defaultVisibleApps?14:0)
195+
.padding(.bottom,5)
196+
.padding(.top,10)
197+
}
198+
}
199+
200+
structMenuItemIcons:View{
201+
@EnvironmentObjectvarstate:AppState
202+
@Environment(\.openURL)privatevaropenURL
203+
204+
letitem:VPNMenuItem
205+
letwsURL:URL
206+
207+
@StateprivatevarcopyIsSelected:Bool=false
208+
@StateprivatevarwebIsSelected:Bool=false
209+
210+
func copyToClipboard(){
211+
letprimaryHost= item.primaryHost(hostnameSuffix: state.hostnameSuffix)
212+
NSPasteboard.general.clearContents()
213+
NSPasteboard.general.setString(primaryHost, forType:.string)
214+
}
215+
216+
varbody:someView{
217+
StatusDot(color: item.status.color)
218+
.padding(.trailing,3)
219+
.padding(.top,1)
220+
MenuItemIconButton(systemName:"doc.on.doc", action: copyToClipboard)
221+
.font(.system(size:9))
222+
.symbolVariant(.fill)
223+
MenuItemIconButton(systemName:"globe", action:{openURL(wsURL)})
224+
.contentShape(Rectangle())
225+
.font(.system(size:12))
226+
.padding(.trailing,Theme.Size.trayMargin)
227+
}
228+
}
229+
230+
structMenuItemIconButton:View{
231+
letsystemName:String
232+
@StatevarisSelected:Bool=false
233+
letaction:@MainActor()->Void
234+
235+
varbody:someView{
236+
Button{
237+
action()
238+
} label:{
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+
.animation(.easeInOut(duration:Theme.Animation.collapsibleDuration), value: isExpanded)
260+
}
261+
}
262+
263+
#if DEBUG
264+
#Preview{
265+
letappState=AppState(persistent:false)
266+
appState.login(baseAccessURL:URL(string:"http://127.0.0.1:8080")!, sessionToken:"")
267+
// appState.clearSession()
268+
269+
returnVPNMenu<PreviewVPN,PreviewFileSync>().frame(width:256)
270+
.environmentObject(PreviewVPN())
271+
.environmentObject(appState)
272+
.environmentObject(PreviewFileSync())
273+
}
274+
#endif

‎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

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp