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

Commit5f067b6

Browse files
feat: add workspace apps (#136)
Closes#94.<img width="311" alt="Screenshot 2025-04-22 at 2 10 32 pm" src="https://github.com/user-attachments/assets/36e20e2e-49b4-4cbd-8bcc-e41840fdc45c" />https://github.com/user-attachments/assets/0777d1c9-6183-487d-b24a-b2ad9639d75bThe cursor does not change to a pointing hand as it should when screen-recording, and the display name of the app is also shown on hover:<img width="255" alt="image" src="https://github.com/user-attachments/assets/95c1f06b-b14a-457c-85a6-5a514b017def" />As per the linked issue, this only shows the first five apps. If there's less than 5 apps, they won't be centered (I think this looks a bit better):<img width="325" alt="image" src="https://github.com/user-attachments/assets/348c1b46-f8d5-4a32-8ba6-eb03d8125344" />Later designs will likely include a Workspace window where all the apps can be viewed, and potentially reordered to control what is shown on the tray.EDIT: Web apps have been filtered out of the above examples, as we don't currently have a way to determine whether they will work properly via Coder Connect.
1 parent681a9a6 commit5f067b6

File tree

10 files changed

+680
-37
lines changed

10 files changed

+680
-37
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import FluidMenuBarExtra
22
import NetworkExtension
3+
import SDWebImageSVGCoder
4+
import SDWebImageSwiftUI
35
import SwiftUI
46
import VPNLib
57

@@ -66,6 +68,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
6668
}
6769

6870
func applicationDidFinishLaunching(_:Notification){
71+
// Init SVG loader
72+
SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
73+
6974
menuBar=.init(menuBarExtra:FluidMenuBarExtra(
7075
title:"Coder Desktop",
7176
image:"MenuBarIcon",

‎Coder-Desktop/Coder-Desktop/State.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class AppState: ObservableObject {
3737
}
3838
}
3939

40-
privatevarclient:Client?
40+
publicvarclient:Client?
4141

4242
@PublishedvaruseLiteralHeaders:Bool=UserDefaults.standard.bool(forKey:Keys.useLiteralHeaders){
4343
didSet{

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ enum Theme {
77
staticlettrayInset:CGFloat= trayMargin+ trayPadding
88

99
staticletrectCornerRadius:CGFloat=4
10+
11+
staticletappIconWidth:CGFloat=30
12+
staticletappIconHeight:CGFloat=30
13+
staticletappIconSize:CGSize=.init(width: appIconWidth, height: appIconHeight)
1014
}
1115

1216
staticletdefaultVisibleAgents=5

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,8 @@ struct ResponsiveLink: View {
1313
.font(.subheadline)
1414
.foregroundColor(isPressed?.red:.blue)
1515
.underline(isHovered, color: isPressed?.red:.blue)
16-
.onHover{ hoveringin
16+
.onHoverWithPointingHand{ hoveringin
1717
isHovered= hovering
18-
if hovering{
19-
NSCursor.pointingHand.push()
20-
}else{
21-
NSCursor.pop()
22-
}
2318
}
2419
.simultaneousGesture(
2520
DragGesture(minimumDistance:0)

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,16 @@ extension UUID {
3131
self.init(uuid: uuid)
3232
}
3333
}
34+
35+
publicextensionView{
36+
@inlinablenonisolatedfunc onHoverWithPointingHand(perform action:@escaping(Bool)->Void)->someView{
37+
onHover{ hoveringin
38+
if hovering{
39+
NSCursor.pointingHand.push()
40+
}else{
41+
NSCursor.pop()
42+
}
43+
action(hovering)
44+
}
45+
}
46+
}

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

Lines changed: 98 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import CoderSDK
2+
import os
13
import SwiftUI
24

35
// Each row in the workspaces list is an agent or an offline workspace
@@ -26,6 +28,13 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
2628
}
2729
}
2830

31+
varworkspaceID:UUID{
32+
switchself{
33+
caselet.agent(agent): agent.wsID
34+
caselet.offlineWorkspace(workspace): workspace.id
35+
}
36+
}
37+
2938
staticfunc<(lhs:VPNMenuItem, rhs:VPNMenuItem)->Bool{
3039
switch(lhs, rhs){
3140
caselet(.agent(lhsAgent),.agent(rhsAgent)):
@@ -44,11 +53,17 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
4453
structMenuItemView:View{
4554
@EnvironmentObjectvarstate:AppState
4655

56+
privateletlogger=Logger(subsystem:Bundle.main.bundleIdentifier!, category:"VPNMenu")
57+
4758
letitem:VPNMenuItem
4859
letbaseAccessURL:URL
60+
4961
@StateprivatevarnameIsSelected:Bool=false
5062
@StateprivatevarcopyIsSelected:Bool=false
5163

64+
privateletdefaultVisibleApps=5
65+
@Stateprivatevarapps:[WorkspaceApp]=[]
66+
5267
privatevaritemName:AttributedString{
5368
letname=switch item{
5469
caselet.agent(agent): agent.primaryHost??"\(item.wsName).\(state.hostnameSuffix)"
@@ -70,37 +85,90 @@ struct MenuItemView: View {
7085
}
7186

7287
varbody:someView{
73-
HStack(spacing:0){
74-
Link(destination: wsURL){
75-
HStack(spacing:Theme.Size.trayPadding){
76-
StatusDot(color: item.status.color)
77-
Text(itemName).lineLimit(1).truncationMode(.tail)
88+
VStack(spacing:0){
89+
HStack(spacing:0){
90+
Link(destination: wsURL){
91+
HStack(spacing:Theme.Size.trayPadding){
92+
StatusDot(color: item.status.color)
93+
Text(itemName).lineLimit(1).truncationMode(.tail)
94+
Spacer()
95+
}.padding(.horizontal,Theme.Size.trayPadding)
96+
.frame(minHeight:22)
97+
.frame(maxWidth:.infinity, alignment:.leading)
98+
.foregroundStyle(nameIsSelected?.white:.primary)
99+
.background(nameIsSelected?Color.accentColor.opacity(0.8):.clear)
100+
.clipShape(.rect(cornerRadius:Theme.Size.rectCornerRadius))
101+
.onHoverWithPointingHand{ hoveringin
102+
nameIsSelected= hovering
103+
}
78104
Spacer()
79-
}.padding(.horizontal,Theme.Size.trayPadding)
80-
.frame(minHeight:22)
81-
.frame(maxWidth:.infinity, alignment:.leading)
82-
.foregroundStyle(nameIsSelected?.white:.primary)
83-
.background(nameIsSelected?Color.accentColor.opacity(0.8):.clear)
84-
.clipShape(.rect(cornerRadius:Theme.Size.rectCornerRadius))
85-
.onHover{ hoveringin nameIsSelected= hovering}
86-
Spacer()
87-
}.buttonStyle(.plain)
88-
if caselet.agent(agent)= item,let copyableDNS= agent.primaryHost{
89-
Button{
90-
NSPasteboard.general.clearContents()
91-
NSPasteboard.general.setString(copyableDNS, forType:.string)
92-
} label:{
93-
Image(systemName:"doc.on.doc")
94-
.symbolVariant(.fill)
95-
.padding(3)
96-
.contentShape(Rectangle())
97-
}.foregroundStyle(copyIsSelected?.white:.primary)
98-
.imageScale(.small)
99-
.background(copyIsSelected?Color.accentColor.opacity(0.8):.clear)
100-
.clipShape(.rect(cornerRadius:Theme.Size.rectCornerRadius))
101-
.onHover{ hoveringin copyIsSelected= hovering}
102-
.buttonStyle(.plain)
103-
.padding(.trailing,Theme.Size.trayMargin)
105+
}.buttonStyle(.plain)
106+
if caselet.agent(agent)= item,let copyableDNS= agent.primaryHost{
107+
Button{
108+
NSPasteboard.general.clearContents()
109+
NSPasteboard.general.setString(copyableDNS, 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+
}
123+
}
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()
132+
}
133+
}
134+
.padding(.leading, apps.count< defaultVisibleApps?14:0)
135+
.padding(.bottom,5)
136+
.padding(.top,10)
137+
}
138+
}
139+
.task{awaitloadApps()}
140+
}
141+
142+
func loadApps()async{
143+
// If this menu item is an agent, and the user is logged in
144+
if caselet.agent(agent)= item,
145+
let client= state.client,
146+
let host= agent.primaryHost,
147+
let baseAccessURL= state.baseAccessURL,
148+
// Like the CLI, we'll re-use the existing session token to populate the URL
149+
let sessionToken= state.sessionToken
150+
{
151+
letworkspace:CoderSDK.Workspace
152+
do{
153+
workspace=tryawaitretry(floor:.milliseconds(100), ceil:.seconds(10)){
154+
do{
155+
returntryawait client.workspace(item.workspaceID)
156+
}catch{
157+
logger.error("Failed to load apps for workspace\(item.wsName):\(error.localizedDescription)")
158+
throw error
159+
}
160+
}
161+
}catch{return} // Task cancelled
162+
163+
iflet wsAgent= workspace
164+
.latest_build.resources
165+
.compactMap(\.agents)
166+
.flatMap(\.self)
167+
.first(where:{ $0.id== agent.id})
168+
{
169+
apps=agentToApps(logger, wsAgent, host, baseAccessURL, sessionToken)
170+
}else{
171+
logger.error("Could not find agent '\(agent.id)' in workspace '\(item.wsName)' resources")
104172
}
105173
}
106174
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp