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

Commit059179c

Browse files
authored
added new settings dialog + settings manager (#113)
Closes:#57 &#55Adds:- **SettingsManager** that manages settings located in AppData- **Settings** views to manage the settings- **StartupManager** that allows to control registry access to enableload on startup![image](https://github.com/user-attachments/assets/deb834cb-44fd-4282-8db8-918bd11b1ab8)
1 parentd49de5b commit059179c

14 files changed

+669
-57
lines changed

‎App/App.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
<ItemGroup>
5858
<PackageReferenceInclude="CommunityToolkit.Mvvm"Version="8.4.0" />
5959
<PackageReferenceInclude="CommunityToolkit.WinUI.Controls.Primitives"Version="8.2.250402" />
60+
<PackageReferenceInclude="CommunityToolkit.WinUI.Controls.SettingsControls"Version="8.2.250402" />
6061
<PackageReferenceInclude="CommunityToolkit.WinUI.Extensions"Version="8.2.250402" />
6162
<PackageReferenceInclude="DependencyPropertyGenerator"Version="1.5.0">
6263
<PrivateAssets>all</PrivateAssets>

‎App/App.xaml.cs

Lines changed: 84 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
usingSystem;
22
usingSystem.Collections.Generic;
3-
usingSystem.Diagnostics;
43
usingSystem.IO;
54
usingSystem.Threading;
65
usingSystem.Threading.Tasks;
@@ -44,6 +43,10 @@ public partial class App : Application
4443
privatereadonlyILogger<App>_logger;
4544
privatereadonlyIUriHandler_uriHandler;
4645

46+
privatereadonlyISettingsManager<CoderConnectSettings>_settingsManager;
47+
48+
privatereadonlyIHostApplicationLifetime_appLifetime;
49+
4750
publicApp()
4851
{
4952
varbuilder=Host.CreateApplicationBuilder();
@@ -90,6 +93,13 @@ public App()
9093
// FileSyncListMainPage is created by FileSyncListWindow.
9194
services.AddTransient<FileSyncListWindow>();
9295

96+
services.AddSingleton<ISettingsManager<CoderConnectSettings>,SettingsManager<CoderConnectSettings>>();
97+
services.AddSingleton<IStartupManager,StartupManager>();
98+
// SettingsWindow views and view models
99+
services.AddTransient<SettingsViewModel>();
100+
// SettingsMainPage is created by SettingsWindow.
101+
services.AddTransient<SettingsWindow>();
102+
93103
// DirectoryPickerWindow views and view models are created by FileSyncListViewModel.
94104

95105
// TrayWindow views and view models
@@ -107,8 +117,10 @@ public App()
107117
services.AddTransient<TrayWindow>();
108118

109119
_services=services.BuildServiceProvider();
110-
_logger=(ILogger<App>)_services.GetService(typeof(ILogger<App>))!;
111-
_uriHandler=(IUriHandler)_services.GetService(typeof(IUriHandler))!;
120+
_logger=_services.GetRequiredService<ILogger<App>>();
121+
_uriHandler=_services.GetRequiredService<IUriHandler>();
122+
_settingsManager=_services.GetRequiredService<ISettingsManager<CoderConnectSettings>>();
123+
_appLifetime=_services.GetRequiredService<IHostApplicationLifetime>();
112124

113125
InitializeComponent();
114126
}
@@ -129,58 +141,8 @@ public async Task ExitApplication()
129141
protectedoverridevoidOnLaunched(LaunchActivatedEventArgsargs)
130142
{
131143
_logger.LogInformation("new instance launched");
132-
// Start connecting to the manager in the background.
133-
varrpcController=_services.GetRequiredService<IRpcController>();
134-
if(rpcController.GetState().RpcLifecycle==RpcLifecycle.Disconnected)
135-
// Passing in a CT with no cancellation is desired here, because
136-
// the named pipe open will block until the pipe comes up.
137-
_logger.LogDebug("reconnecting with VPN service");
138-
_=rpcController.Reconnect(CancellationToken.None).ContinueWith(t=>
139-
{
140-
if(t.Exception!=null)
141-
{
142-
_logger.LogError(t.Exception,"failed to connect to VPN service");
143-
#ifDEBUG
144-
Debug.WriteLine(t.Exception);
145-
Debugger.Break();
146-
#endif
147-
}
148-
});
149-
150-
// Load the credentials in the background.
151-
varcredentialManagerCts=newCancellationTokenSource(TimeSpan.FromSeconds(15));
152-
varcredentialManager=_services.GetRequiredService<ICredentialManager>();
153-
_=credentialManager.LoadCredentials(credentialManagerCts.Token).ContinueWith(t=>
154-
{
155-
if(t.Exception!=null)
156-
{
157-
_logger.LogError(t.Exception,"failed to load credentials");
158-
#ifDEBUG
159-
Debug.WriteLine(t.Exception);
160-
Debugger.Break();
161-
#endif
162-
}
163144

164-
credentialManagerCts.Dispose();
165-
},CancellationToken.None);
166-
167-
// Initialize file sync.
168-
// We're adding a 5s delay here to avoid race conditions when loading the mutagen binary.
169-
170-
_=Task.Delay(5000).ContinueWith((_)=>
171-
{
172-
varsyncSessionCts=newCancellationTokenSource(TimeSpan.FromSeconds(10));
173-
varsyncSessionController=_services.GetRequiredService<ISyncSessionController>();
174-
syncSessionController.RefreshState(syncSessionCts.Token).ContinueWith(
175-
t=>
176-
{
177-
if(t.IsCanceled||t.Exception!=null)
178-
{
179-
_logger.LogError(t.Exception,"failed to refresh sync state (canceled = {canceled})",t.IsCanceled);
180-
}
181-
syncSessionCts.Dispose();
182-
},CancellationToken.None);
183-
});
145+
_=InitializeServicesAsync(_appLifetime.ApplicationStopping);
184146

185147
// Prevent the TrayWindow from closing, just hide it.
186148
vartrayWindow=_services.GetRequiredService<TrayWindow>();
@@ -192,6 +154,74 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
192154
};
193155
}
194156

157+
/// <summary>
158+
/// Loads stored VPN credentials, reconnects the RPC controller,
159+
/// and (optionally) starts the VPN tunnel on application launch.
160+
/// </summary>
161+
privateasyncTaskInitializeServicesAsync(CancellationTokencancellationToken=default)
162+
{
163+
varcredentialManager=_services.GetRequiredService<ICredentialManager>();
164+
varrpcController=_services.GetRequiredService<IRpcController>();
165+
166+
usingvarcredsCts=CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
167+
credsCts.CancelAfter(TimeSpan.FromSeconds(15));
168+
169+
varloadCredsTask=credentialManager.LoadCredentials(credsCts.Token);
170+
varreconnectTask=rpcController.Reconnect(cancellationToken);
171+
varsettingsTask=_settingsManager.Read(cancellationToken);
172+
173+
vardependenciesLoaded=true;
174+
175+
try
176+
{
177+
awaitTask.WhenAll(loadCredsTask,reconnectTask,settingsTask);
178+
}
179+
catch(Exception)
180+
{
181+
if(loadCredsTask.IsFaulted)
182+
_logger.LogError(loadCredsTask.Exception!.GetBaseException(),
183+
"Failed to load credentials");
184+
185+
if(reconnectTask.IsFaulted)
186+
_logger.LogError(reconnectTask.Exception!.GetBaseException(),
187+
"Failed to connect to VPN service");
188+
189+
if(settingsTask.IsFaulted)
190+
_logger.LogError(settingsTask.Exception!.GetBaseException(),
191+
"Failed to fetch Coder Connect settings");
192+
193+
// Don't attempt to connect if we failed to load credentials or reconnect.
194+
// This will prevent the app from trying to connect to the VPN service.
195+
dependenciesLoaded=false;
196+
}
197+
198+
varattemptCoderConnection=settingsTask.Result?.ConnectOnLaunch??false;
199+
if(dependenciesLoaded&&attemptCoderConnection)
200+
{
201+
try
202+
{
203+
awaitrpcController.StartVpn(cancellationToken);
204+
}
205+
catch(Exceptionex)
206+
{
207+
_logger.LogError(ex,"Failed to connect on launch");
208+
}
209+
}
210+
211+
// Initialize file sync.
212+
usingvarsyncSessionCts=CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
213+
syncSessionCts.CancelAfter(TimeSpan.FromSeconds(10));
214+
varsyncSessionController=_services.GetRequiredService<ISyncSessionController>();
215+
try
216+
{
217+
awaitsyncSessionController.RefreshState(syncSessionCts.Token);
218+
}
219+
catch(Exceptionex)
220+
{
221+
_logger.LogError($"Failed to refresh sync session state{ex.Message}",ex);
222+
}
223+
}
224+
195225
publicvoidOnActivated(object?sender,AppActivationArgumentsargs)
196226
{
197227
switch(args.Kind)

‎App/Models/Settings.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
namespaceCoder.Desktop.App.Models;
2+
3+
publicinterfaceISettings<T>:ICloneable<T>
4+
{
5+
/// <summary>
6+
/// FileName where the settings are stored.
7+
/// </summary>
8+
staticabstractstringSettingsFileName{get;}
9+
10+
/// <summary>
11+
/// Gets the version of the settings schema.
12+
/// </summary>
13+
intVersion{get;}
14+
}
15+
16+
publicinterfaceICloneable<T>
17+
{
18+
/// <summary>
19+
/// Creates a deep copy of the settings object.
20+
/// </summary>
21+
/// <returns>A new instance of the settings object with the same values.</returns>
22+
TClone();
23+
}
24+
25+
/// <summary>
26+
/// CoderConnect settings class that holds the settings for the CoderConnect feature.
27+
/// </summary>
28+
publicclassCoderConnectSettings:ISettings<CoderConnectSettings>
29+
{
30+
publicstaticstringSettingsFileName{get;}="coder-connect-settings.json";
31+
publicintVersion{get;set;}
32+
/// <summary>
33+
/// When this is true, CoderConnect will automatically connect to the Coder VPN when the application starts.
34+
/// </summary>
35+
publicboolConnectOnLaunch{get;set;}
36+
37+
/// <summary>
38+
/// CoderConnect current settings version. Increment this when the settings schema changes.
39+
/// In future iterations we will be able to handle migrations when the user has
40+
/// an older version.
41+
/// </summary>
42+
privateconstintVERSION=1;
43+
44+
publicCoderConnectSettings()
45+
{
46+
Version=VERSION;
47+
48+
ConnectOnLaunch=false;
49+
}
50+
51+
publicCoderConnectSettings(int?version,boolconnectOnLaunch)
52+
{
53+
Version=version??VERSION;
54+
55+
ConnectOnLaunch=connectOnLaunch;
56+
}
57+
58+
publicCoderConnectSettingsClone()
59+
{
60+
returnnewCoderConnectSettings(Version,ConnectOnLaunch);
61+
}
62+
}

‎App/Services/SettingsManager.cs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
usingSystem;
2+
usingSystem.IO;
3+
usingSystem.Text.Json;
4+
usingSystem.Threading;
5+
usingSystem.Threading.Tasks;
6+
usingCoder.Desktop.App.Models;
7+
8+
namespaceCoder.Desktop.App.Services;
9+
10+
/// <summary>
11+
/// Settings contract exposing properties for app settings.
12+
/// </summary>
13+
publicinterfaceISettingsManager<T>whereT:ISettings<T>,new()
14+
{
15+
/// <summary>
16+
/// Reads the settings from the file system or returns from cache if available.
17+
/// Returned object is always a cloned instance, so it can be modified without affecting the stored settings.
18+
/// </summary>
19+
/// <param name="ct"></param>
20+
/// <returns></returns>
21+
Task<T>Read(CancellationTokenct=default);
22+
/// <summary>
23+
/// Writes the settings to the file system.
24+
/// </summary>
25+
/// <param name="settings">Object containing the settings.</param>
26+
/// <param name="ct"></param>
27+
/// <returns></returns>
28+
TaskWrite(Tsettings,CancellationTokenct=default);
29+
}
30+
31+
/// <summary>
32+
/// Implemention of <see cref="ISettingsManager"/> that persists settings to a JSON file
33+
/// located in the user's local application data folder.
34+
/// </summary>
35+
publicsealedclassSettingsManager<T>:ISettingsManager<T>whereT:ISettings<T>,new()
36+
{
37+
privatereadonlystring_settingsFilePath;
38+
privatereadonlystring_appName="CoderDesktop";
39+
privatestring_fileName;
40+
41+
privateT?_cachedSettings;
42+
43+
privatereadonlySemaphoreSlim_gate=new(1,1);
44+
privatestaticreadonlyTimeSpanLockTimeout=TimeSpan.FromSeconds(3);
45+
46+
/// <param name="settingsFilePath">
47+
/// For unit‑tests you can pass an absolute path that already exists.
48+
/// Otherwise the settings file will be created in the user's local application data folder.
49+
/// </param>
50+
publicSettingsManager(string?settingsFilePath=null)
51+
{
52+
if(settingsFilePathisnull)
53+
{
54+
settingsFilePath=Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
55+
}
56+
elseif(!Path.IsPathRooted(settingsFilePath))
57+
{
58+
thrownewArgumentException("settingsFilePath must be an absolute path if provided",nameof(settingsFilePath));
59+
}
60+
61+
varfolder=Path.Combine(
62+
settingsFilePath,
63+
_appName);
64+
65+
Directory.CreateDirectory(folder);
66+
67+
_fileName=T.SettingsFileName;
68+
_settingsFilePath=Path.Combine(folder,_fileName);
69+
}
70+
71+
publicasyncTask<T>Read(CancellationTokenct=default)
72+
{
73+
if(_cachedSettingsis notnull)
74+
{
75+
// return cached settings if available
76+
return_cachedSettings.Clone();
77+
}
78+
79+
// try to get the lock with short timeout
80+
if(!await_gate.WaitAsync(LockTimeout,ct).ConfigureAwait(false))
81+
thrownewInvalidOperationException(
82+
$"Could not acquire the settings lock within{LockTimeout.TotalSeconds} s.");
83+
84+
try
85+
{
86+
if(!File.Exists(_settingsFilePath))
87+
returnnew();
88+
89+
varjson=awaitFile.ReadAllTextAsync(_settingsFilePath,ct)
90+
.ConfigureAwait(false);
91+
92+
// deserialize; fall back to default(T) if empty or malformed
93+
varresult=JsonSerializer.Deserialize<T>(json)!;
94+
_cachedSettings=result;
95+
return_cachedSettings.Clone();// return a fresh instance of the settings
96+
}
97+
catch(OperationCanceledException)
98+
{
99+
throw;// propagate caller-requested cancellation
100+
}
101+
catch(Exceptionex)
102+
{
103+
thrownewInvalidOperationException(
104+
$"Failed to read settings from{_settingsFilePath}. "+
105+
"The file may be corrupted, malformed or locked.",ex);
106+
}
107+
finally
108+
{
109+
_gate.Release();
110+
}
111+
}
112+
113+
publicasyncTaskWrite(Tsettings,CancellationTokenct=default)
114+
{
115+
// try to get the lock with short timeout
116+
if(!await_gate.WaitAsync(LockTimeout,ct).ConfigureAwait(false))
117+
thrownewInvalidOperationException(
118+
$"Could not acquire the settings lock within{LockTimeout.TotalSeconds} s.");
119+
120+
try
121+
{
122+
// overwrite the settings file with the new settings
123+
varjson=JsonSerializer.Serialize(
124+
settings,newJsonSerializerOptions(){WriteIndented=true});
125+
_cachedSettings=settings;// cache the settings
126+
awaitFile.WriteAllTextAsync(_settingsFilePath,json,ct)
127+
.ConfigureAwait(false);
128+
}
129+
catch(OperationCanceledException)
130+
{
131+
throw;// let callers observe cancellation
132+
}
133+
catch(Exceptionex)
134+
{
135+
thrownewInvalidOperationException(
136+
$"Failed to persist settings to{_settingsFilePath}. "+
137+
"The file may be corrupted, malformed or locked.",ex);
138+
}
139+
finally
140+
{
141+
_gate.Release();
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp