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

Commitae6eb95

Browse files
committed
progress gauge
1 parent4d87c0c commitae6eb95

File tree

10 files changed

+223
-39
lines changed

10 files changed

+223
-39
lines changed

‎Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ final class PreviewVPN: Coder_Desktop.VPNService {
3333
self.shouldFail= shouldFail
3434
}
3535

36-
@PublishedvarprogressMessage:String?
36+
@Publishedvarprogress:VPNProgress=.init(stage:.none, downloadProgress:nil)
3737

3838
varstartTask:Task<Void,Never>?
3939
func start()async{
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import SwiftUI
2+
import VPNLib
3+
4+
structVPNProgress{
5+
letstage:ProgressStage
6+
letdownloadProgress:DownloadProgress?
7+
}
8+
9+
structVPNProgressView:View{
10+
letstate:VPNServiceState
11+
letprogress:VPNProgress
12+
13+
varbody:someView{
14+
VStack{
15+
CircularProgressView(value: value)
16+
// We'll estimate that the last 25% takes 9 seconds
17+
// so it doesn't appear stuck
18+
.autoComplete(threshold:0.75, duration:9)
19+
Text(progressMessage)
20+
.multilineTextAlignment(.center)
21+
}
22+
.padding()
23+
.progressViewStyle(.circular)
24+
.foregroundStyle(.secondary)
25+
}
26+
27+
varprogressMessage:String{
28+
"\(progress.stage.description?? defaultMessage)\(downloadProgressMessage)"
29+
}
30+
31+
vardownloadProgressMessage:String{
32+
progress.downloadProgress.flatMap{"\n\($0.description)"}??""
33+
}
34+
35+
vardefaultMessage:String{
36+
state==.connecting?"Starting Coder Connect...":"Stopping Coder Connect..."
37+
}
38+
39+
varvalue:Float?{
40+
guard state==.connectingelse{
41+
returnnil
42+
}
43+
switch progress.stage{
44+
case.none:
45+
return0.10
46+
case.downloading:
47+
guardlet downloadProgress= progress.downloadProgresselse{
48+
// We can't make this illegal state unrepresentable because XPC
49+
// doesn't support enums with associated values.
50+
return0.05
51+
}
52+
// 40MB if the server doesn't give us the expected size
53+
lettotalBytes= downloadProgress.totalBytesToWrite??40_000_000
54+
letdownloadPercent=min(1.0,Float(downloadProgress.totalBytesWritten)/ Float(totalBytes))
55+
return0.10+0.6* downloadPercent
56+
case.validating:
57+
return0.71
58+
case.removingQuarantine:
59+
return0.72
60+
case.opening:
61+
return0.73
62+
case.settingUpTunnel:
63+
return0.74
64+
case.startingTunnel:
65+
return0.75
66+
}
67+
}
68+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import VPNLib
77
protocolVPNService:ObservableObject{
88
varstate:VPNServiceState{get}
99
varmenuState:VPNMenuState{get}
10-
varprogressMessage:String?{get}
10+
varprogress:VPNProgress{get}
1111
func start()async
1212
func stop()async
1313
func configureTunnelProviderProtocol(proto:NETunnelProviderProtocol?)
@@ -73,7 +73,7 @@ final class CoderVPNService: NSObject, VPNService {
7373
return tunnelState
7474
}
7575

76-
@PublishedvarprogressMessage:String?
76+
@Publishedvarprogress:VPNProgress=.init(stage:.none, downloadProgress:nil)
7777

7878
@PublishedvarmenuState:VPNMenuState=.init()
7979

@@ -158,8 +158,8 @@ final class CoderVPNService: NSObject, VPNService {
158158
}
159159
}
160160

161-
func onProgress(_ msg:String?){
162-
progressMessage=msg
161+
func onProgress(stage:ProgressStage, downloadProgress:DownloadProgress?){
162+
progress=.init(stage: stage, downloadProgress: downloadProgress)
163163
}
164164

165165
func applyPeerUpdate(with update:Vpn_PeerUpdate){
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import SwiftUI
2+
3+
structCircularProgressView:View{
4+
letvalue:Float?
5+
6+
varstrokeWidth:CGFloat=4
7+
vardiameter:CGFloat=22
8+
varprimaryColor:Color=.secondary
9+
varbackgroundColor:Color=.secondary.opacity(0.3)
10+
11+
@Stateprivatevarrotation=0.0
12+
@StateprivatevartrimAmount:CGFloat=0.15
13+
14+
varautoCompleteThreshold:Float?
15+
varautoCompleteDuration:TimeInterval?
16+
17+
varbody:someView{
18+
ZStack{
19+
// Background circle
20+
Circle()
21+
.stroke(backgroundColor, style:StrokeStyle(lineWidth: strokeWidth, lineCap:.round))
22+
.frame(width: diameter, height: diameter)
23+
Group{
24+
iflet value{
25+
// Determinate gauge
26+
Circle()
27+
.trim(from:0, to:CGFloat(displayValue(for: value)))
28+
.stroke(primaryColor, style:StrokeStyle(lineWidth: strokeWidth, lineCap:.round))
29+
.frame(width: diameter, height: diameter)
30+
.rotationEffect(.degrees(-90))
31+
.animation(autoCompleteAnimation(for: value), value: value)
32+
}else{
33+
// Indeterminate gauge
34+
Circle()
35+
.trim(from:0, to: trimAmount)
36+
.stroke(primaryColor, style:StrokeStyle(lineWidth: strokeWidth, lineCap:.round))
37+
.frame(width: diameter, height: diameter)
38+
.rotationEffect(.degrees(rotation))
39+
}
40+
}
41+
}
42+
.frame(width: diameter+ strokeWidth*2, height: diameter+ strokeWidth*2)
43+
.onAppear{
44+
if value==nil{
45+
withAnimation(.linear(duration:0.8).repeatForever(autoreverses:false)){
46+
rotation=360
47+
}
48+
}
49+
}
50+
}
51+
52+
privatefunc displayValue(for value:Float)->Float{
53+
iflet threshold= autoCompleteThreshold,
54+
value>= threshold, value<1.0
55+
{
56+
return1.0
57+
}
58+
return value
59+
}
60+
61+
privatefunc autoCompleteAnimation(for value:Float)->Animation?{
62+
guardlet threshold= autoCompleteThreshold,
63+
let duration= autoCompleteDuration,
64+
value>= threshold, value<1.0
65+
else{
66+
return.default
67+
}
68+
69+
return.easeOut(duration: duration)
70+
}
71+
}
72+
73+
extensionCircularProgressView{
74+
func autoComplete(threshold:Float, duration:TimeInterval)->CircularProgressView{
75+
varview=self
76+
view.autoCompleteThreshold= threshold
77+
view.autoCompleteDuration= duration
78+
return view
79+
}
80+
}

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@ struct VPNState<VPN: VPNService>: View {
66

77
letinspection=Inspection<Self>()
88

9-
varprogressMessage:String{
10-
iflet msg= vpn.progressMessage{
11-
msg
12-
}else{
13-
vpn.state==.connecting?"Starting Coder Connect...":"Stopping Coder Connect..."
14-
}
15-
}
16-
179
varbody:someView{
1810
Group{
1911
switch(vpn.state, state.hasSession){
@@ -36,11 +28,7 @@ struct VPNState<VPN: VPNService>: View {
3628
case(.connecting, _),(.disconnecting, _):
3729
HStack{
3830
Spacer()
39-
ProgressView{
40-
Text(progressMessage)
41-
.multilineTextAlignment(.center)
42-
}
43-
.padding()
31+
VPNProgressView(state: vpn.state, progress: vpn.progress)
4432
Spacer()
4533
}
4634
caselet(.failed(vpnErr), _):

‎Coder-Desktop/Coder-Desktop/XPCInterface.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ import VPNLib
7171
}
7272
}
7373

74-
func onProgress(msg:String?){
74+
func onProgress(stage:ProgressStage, downloadProgress:DownloadProgress?){
7575
Task{@MainActorin
76-
svc.onProgress(msg)
76+
svc.onProgress(stage: stage, downloadProgress: downloadProgress)
7777
}
7878
}
7979

‎Coder-Desktop/VPN/Manager.swift

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ actor Manager {
4040
dest: dest,
4141
urlSession:URLSession(configuration: sessionConfig)
4242
){ progressin
43-
pushProgress(msg:"Downloading library...\n\(progress.description)")
43+
// TODO: Debounce, somehow
44+
pushProgress(stage:.downloading, downloadProgress: progress)
4445
}
4546
}catch{
4647
throw.download(error)
4748
}
48-
pushProgress(msg:"Fetching server version...")
49+
pushProgress(stage:.validating)
4950
letclient=Client(url: cfg.serverUrl)
5051
letbuildInfo:BuildInfoResponse
5152
do{
@@ -56,7 +57,6 @@ actor Manager {
5657
guardlet semver= buildInfo.semverelse{
5758
throw.serverInfo("invalid version:\(buildInfo.version)")
5859
}
59-
pushProgress(msg:"Validating library...")
6060
do{
6161
trySignatureValidator.validate(path: dest, expectedVersion: semver)
6262
}catch{
@@ -67,13 +67,13 @@ actor Manager {
6767
// so it's safe to execute. However, the SE must be sandboxed, so we defer to the app.
6868
tryawaitremoveQuarantine(dest)
6969

70-
pushProgress(msg:"Opening library...")
70+
pushProgress(stage:.opening)
7171
do{
7272
try tunnelHandle=TunnelHandle(dylibPath: dest)
7373
}catch{
7474
throw.tunnelSetup(error)
7575
}
76-
pushProgress(msg:"Setting up tunnel...")
76+
pushProgress(stage:.settingUpTunnel)
7777
speaker=awaitSpeaker<Vpn_ManagerMessage,Vpn_TunnelMessage>(
7878
writeFD: tunnelHandle.writeHandle,
7979
readFD: tunnelHandle.readHandle
@@ -168,8 +168,7 @@ actor Manager {
168168
}
169169

170170
func startVPN()asyncthrows(ManagerError){
171-
// Clear progress message
172-
pushProgress(msg:nil)
171+
pushProgress(stage:.startingTunnel)
173172
logger.info("sending start rpc")
174173
guardlet tunFd= ptp.tunnelFileDescriptorelse{
175174
logger.error("no fd")
@@ -246,13 +245,13 @@ actor Manager {
246245
}
247246
}
248247

249-
func pushProgress(msg:String?){
248+
func pushProgress(stage:ProgressStage, downloadProgress:DownloadProgress?=nil){
250249
guardlet conn= globalXPCListenerDelegate.connelse{
251250
logger.warning("couldn't send progress message to app: no connection")
252251
return
253252
}
254-
logger.debug("sending progress message to app:\(msg??"nil")")
255-
conn.onProgress(msg: msg)
253+
logger.debug("sending progress message to app")
254+
conn.onProgress(stage: stage, downloadProgress: downloadProgress)
256255
}
257256

258257
structManagerConfig{
@@ -333,7 +332,7 @@ private func removeQuarantine(_ dest: URL) async throws(ManagerError) {
333332
letfile=NSURL(fileURLWithPath: dest.path)
334333
try? file.getResourceValue(&flag, forKey: kCFURLQuarantinePropertiesKeyasURLResourceKey)
335334
if flag!=nil{
336-
pushProgress(msg:"Unquarantining download...")
335+
pushProgress(stage:.removingQuarantine)
337336
// Try the privileged helper first (it may not even be registered)
338337
ifawait globalHelperXPCSpeaker.tryRemoveQuarantine(path: dest.path){
339338
// Success!

‎Coder-Desktop/VPN/PacketTunnelProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
9393
self.manager= manager
9494
completionHandler(nil)
9595
// Clear progress message
96-
pushProgress(msg:nil)
96+
pushProgress(stage:.none, downloadProgress:nil)
9797
} catch{
9898
logger.error("error starting manager:\(error.description, privacy:.public)")
9999
completionHandler(

‎Coder-Desktop/VPNLib/Download.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,14 +251,34 @@ extension DownloadManager: URLSessionDownloadDelegate {
251251
}
252252
}
253253

254-
publicstructDownloadProgress:Sendable,CustomStringConvertible{
255-
lettotalBytesWritten:Int64
256-
lettotalBytesToWrite:Int64?
254+
@objcpublicfinalclassDownloadProgress:NSObject,NSSecureCoding,@uncheckedSendable{
255+
publicstaticvarsupportsSecureCoding:Bool{true}
257256

258-
publicvardescription:String{
257+
publiclettotalBytesWritten:Int64
258+
publiclettotalBytesToWrite:Int64?
259+
260+
publicinit(totalBytesWritten:Int64, totalBytesToWrite:Int64?){
261+
self.totalBytesWritten= totalBytesWritten
262+
self.totalBytesToWrite= totalBytesToWrite
263+
}
264+
265+
publicrequiredconvenienceinit?(coder:NSCoder){
266+
letwritten= coder.decodeInt64(forKey:"written")
267+
lettotal= coder.containsValue(forKey:"total")? coder.decodeInt64(forKey:"total"):nil
268+
self.init(totalBytesWritten: written, totalBytesToWrite: total)
269+
}
270+
271+
publicfunc encode(with coder:NSCoder){
272+
coder.encode(totalBytesWritten, forKey:"written")
273+
iflet total= totalBytesToWrite{
274+
coder.encode(total, forKey:"total")
275+
}
276+
}
277+
278+
overridepublicvardescription:String{
259279
letfmt=ByteCountFormatter()
260280
letdone= fmt.string(fromByteCount: totalBytesWritten)
261-
lettotal= totalBytesToWrite.map{ fmt.string(fromByteCount: $0)}??"Unknown"
262-
return"\(done) /\(total)"
281+
lettot= totalBytesToWrite.map{ fmt.string(fromByteCount: $0)}??"Unknown"
282+
return"\(done) /\(tot)"
263283
}
264284
}

‎Coder-Desktop/VPNLib/XPC.swift

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,35 @@ import Foundation
1010
@objcpublicprotocolVPNXPCClientCallbackProtocol{
1111
// data is a serialized `Vpn_PeerUpdate`
1212
func onPeerUpdate(_ data:Data)
13-
func onProgress(msg:String?)
13+
func onProgress(stage:ProgressStage, downloadProgress:DownloadProgress?)
1414
func removeQuarantine(path:String, reply:@escaping(Bool)->Void)
1515
}
16+
17+
@objcpublicenumProgressStage:Int,Sendable{
18+
case none
19+
case downloading
20+
case validating
21+
case removingQuarantine
22+
case opening
23+
case settingUpTunnel
24+
case startingTunnel
25+
26+
publicvardescription:String?{
27+
switchself{
28+
case.none:
29+
nil
30+
case.downloading:
31+
"Downloading library..."
32+
case.validating:
33+
"Validating library..."
34+
case.removingQuarantine:
35+
"Removing quarantine..."
36+
case.opening:
37+
"Opening library..."
38+
case.settingUpTunnel:
39+
"Setting up tunnel..."
40+
case.startingTunnel:
41+
nil
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp