Skip to content

Commit 7465bb5

Browse files
committed
added new settings dialog + settings manager
1 parent 74b8658 commit 7465bb5

File tree

10 files changed

+349
-3
lines changed

10 files changed

+349
-3
lines changed

App/App.xaml.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ public App()
9090
// FileSyncListMainPage is created by FileSyncListWindow.
9191
services.AddTransient<FileSyncListWindow>();
9292

93+
services.AddSingleton<ISettingsManager>(_ => new SettingsManager("CoderDesktop"));
94+
// SettingsWindow views and view models
95+
services.AddTransient<SettingsViewModel>();
96+
// SettingsMainPage is created by SettingsWindow.
97+
services.AddTransient<SettingsWindow>();
98+
9399
// DirectoryPickerWindow views and view models are created by FileSyncListViewModel.
94100

95101
// TrayWindow views and view models

App/Services/SettingsManager.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Text.Json;
7+
using System.Threading.Tasks;
8+
9+
namespace Coder.Desktop.App.Services;
10+
/// <summary>
11+
/// Generic persistence contract for simple key/value settings.
12+
/// </summary>
13+
public interface ISettingsManager
14+
{
15+
/// <summary>
16+
/// Saves <paramref name="value"/> under <paramref name="name"/> and returns the value.
17+
/// </summary>
18+
T Save<T>(string name, T value);
19+
20+
/// <summary>
21+
/// Reads the setting or returns <paramref name="defaultValue"/> when the key is missing.
22+
/// </summary>
23+
T Read<T>(string name, T defaultValue);
24+
}
25+
/// <summary>
26+
/// JSON‑file implementation that works in unpackaged Win32/WinUI 3 apps.
27+
/// </summary>
28+
public sealed class SettingsManager : ISettingsManager
29+
{
30+
private readonly string _settingsFilePath;
31+
private readonly string _fileName = "app-settings.json";
32+
private readonly object _lock = new();
33+
private Dictionary<string, JsonElement> _cache;
34+
35+
/// <param name="appName">
36+
/// Sub‑folder under %LOCALAPPDATA% (e.g. "coder-desktop").
37+
/// If <c>null</c> the folder name defaults to the executable name.
38+
/// For unit‑tests you can pass an absolute path that already exists.
39+
/// </param>
40+
public SettingsManager(string? appName = null)
41+
{
42+
// Allow unit‑tests to inject a fully‑qualified path.
43+
if (appName is not null && Path.IsPathRooted(appName))
44+
{
45+
_settingsFilePath = Path.Combine(appName, _fileName);
46+
Directory.CreateDirectory(Path.GetDirectoryName(_settingsFilePath)!);
47+
}
48+
else
49+
{
50+
string folder = Path.Combine(
51+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
52+
appName ?? AppDomain.CurrentDomain.FriendlyName.ToLowerInvariant());
53+
Directory.CreateDirectory(folder);
54+
_settingsFilePath = Path.Combine(folder, _fileName);
55+
}
56+
57+
_cache = Load();
58+
}
59+
60+
public T Save<T>(string name, T value)
61+
{
62+
lock (_lock)
63+
{
64+
_cache[name] = JsonSerializer.SerializeToElement(value);
65+
Persist();
66+
return value;
67+
}
68+
}
69+
70+
public T Read<T>(string name, T defaultValue)
71+
{
72+
lock (_lock)
73+
{
74+
if (_cache.TryGetValue(name, out var element))
75+
{
76+
try
77+
{
78+
return element.Deserialize<T>() ?? defaultValue;
79+
}
80+
catch
81+
{
82+
// Malformed value – fall back.
83+
return defaultValue;
84+
}
85+
}
86+
return defaultValue; // key not found – return caller‑supplied default (false etc.)
87+
}
88+
}
89+
90+
private Dictionary<string, JsonElement> Load()
91+
{
92+
if (!File.Exists(_settingsFilePath))
93+
return new();
94+
95+
try
96+
{
97+
using var fs = File.OpenRead(_settingsFilePath);
98+
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(fs) ?? new();
99+
}
100+
catch
101+
{
102+
// Corrupted file – start fresh.
103+
return new();
104+
}
105+
}
106+
107+
private void Persist()
108+
{
109+
using var fs = File.Create(_settingsFilePath);
110+
var options = new JsonSerializerOptions { WriteIndented = true };
111+
JsonSerializer.Serialize(fs, _cache, options);
112+
}
113+
}

App/ViewModels/SettingsViewModel.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
using Coder.Desktop.App.Services;
3+
using CommunityToolkit.Mvvm.ComponentModel;
4+
using CommunityToolkit.Mvvm.Input;
5+
using Microsoft.UI.Dispatching;
6+
using Microsoft.UI.Xaml;
7+
using Microsoft.UI.Xaml.Controls;
8+
9+
namespace Coder.Desktop.App.ViewModels;
10+
11+
public partial class SettingsViewModel : ObservableObject
12+
{
13+
private Window? _window;
14+
private DispatcherQueue? _dispatcherQueue;
15+
16+
private ISettingsManager _settingsManager;
17+
18+
public SettingsViewModel(ISettingsManager settingsManager)
19+
{
20+
_settingsManager = settingsManager;
21+
}
22+
23+
public void Initialize(Window window, DispatcherQueue dispatcherQueue)
24+
{
25+
_window = window;
26+
_dispatcherQueue = dispatcherQueue;
27+
if (!_dispatcherQueue.HasThreadAccess)
28+
throw new InvalidOperationException("Initialize must be called from the UI thread");
29+
}
30+
31+
[RelayCommand]
32+
private void SaveSetting()
33+
{
34+
//_settingsManager.Save();
35+
}
36+
37+
[RelayCommand]
38+
private void ShowSettingsDialog()
39+
{
40+
if (_window is null || _dispatcherQueue is null)
41+
throw new InvalidOperationException("Initialize must be called before showing the settings dialog.");
42+
// Here you would typically open a settings dialog or page.
43+
// For example, you could navigate to a SettingsPage in your app.
44+
// This is just a placeholder for demonstration purposes.
45+
// Display MessageBox and show a message
46+
var message = $"Settings dialog opened. Current setting: {_settingsManager.Read("SomeSetting", false)}\n" +
47+
"You can implement your settings dialog here.";
48+
var dialog = new ContentDialog();
49+
dialog.Title = "Settings";
50+
dialog.Content = message;
51+
dialog.XamlRoot = _window.Content.XamlRoot;
52+
_ = dialog.ShowAsync();
53+
}
54+
}

App/ViewModels/TrayWindowViewModel.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost
3939

4040
private FileSyncListWindow? _fileSyncListWindow;
4141

42+
private SettingsWindow? _settingsWindow;
43+
4244
private DispatcherQueue? _dispatcherQueue;
4345

4446
// When we transition from 0 online workspaces to >0 online workspaces, the
@@ -359,6 +361,22 @@ private void ShowFileSyncListWindow()
359361
_fileSyncListWindow.Activate();
360362
}
361363

364+
[RelayCommand]
365+
private void ShowSettingsWindow()
366+
{
367+
// This is safe against concurrent access since it all happens in the
368+
// UI thread.
369+
if (_settingsWindow != null)
370+
{
371+
_settingsWindow.Activate();
372+
return;
373+
}
374+
375+
_settingsWindow = _services.GetRequiredService<SettingsWindow>();
376+
_settingsWindow.Closed += (_, _) => _settingsWindow = null;
377+
_settingsWindow.Activate();
378+
}
379+
362380
[RelayCommand]
363381
private async Task SignOut()
364382
{

App/Views/Pages/SettingsMainPage.xaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<Page
4+
x:Class="Coder.Desktop.App.Views.Pages.SettingsMainPage"
5+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
6+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
7+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
8+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
9+
xmlns:viewmodels="using:Coder.Desktop.App.ViewModels"
10+
xmlns:converters="using:Coder.Desktop.App.Converters"
11+
mc:Ignorable="d"
12+
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
13+
14+
<Grid>
15+
<HyperlinkButton
16+
Command="{x:Bind ViewModel.ShowSettingsDialogCommand, Mode=OneWay}"
17+
HorizontalAlignment="Stretch"
18+
HorizontalContentAlignment="Left">
19+
20+
<TextBlock Text="Show settings" Foreground="{ThemeResource DefaultTextForegroundThemeBrush}" />
21+
</HyperlinkButton>
22+
</Grid>
23+
</Page>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Coder.Desktop.App.ViewModels;
2+
using Microsoft.UI.Xaml.Controls;
3+
4+
namespace Coder.Desktop.App.Views.Pages;
5+
6+
public sealed partial class SettingsMainPage : Page
7+
{
8+
public SettingsViewModel ViewModel;
9+
10+
public SettingsMainPage(SettingsViewModel viewModel)
11+
{
12+
ViewModel = viewModel; // already initialized
13+
InitializeComponent();
14+
}
15+
}

App/Views/Pages/TrayWindowMainPage.xaml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
Orientation="Vertical"
2626
HorizontalAlignment="Stretch"
2727
VerticalAlignment="Top"
28-
Padding="20,20,20,30"
28+
Padding="20,20,20,20"
2929
Spacing="10">
3030

3131
<Grid>
@@ -331,9 +331,18 @@
331331

332332
<controls:HorizontalRule />
333333

334+
<HyperlinkButton
335+
Command="{x:Bind ViewModel.ShowSettingsWindowCommand, Mode=OneWay}"
336+
Margin="-12,-4,-12,-4"
337+
HorizontalAlignment="Stretch"
338+
HorizontalContentAlignment="Left">
339+
340+
<TextBlock Text="Settings" Foreground="{ThemeResource DefaultTextForegroundThemeBrush}" />
341+
</HyperlinkButton>
342+
334343
<HyperlinkButton
335344
Command="{x:Bind ViewModel.SignOutCommand, Mode=OneWay}"
336-
Margin="-12,0"
345+
Margin="-12,-4,-12,-4"
337346
HorizontalAlignment="Stretch"
338347
HorizontalContentAlignment="Left">
339348

@@ -342,7 +351,7 @@
342351

343352
<HyperlinkButton
344353
Command="{x:Bind ViewModel.ExitCommand, Mode=OneWay}"
345-
Margin="-12,-8,-12,-5"
354+
Margin="-12,-4,-12,-4"
346355
HorizontalAlignment="Stretch"
347356
HorizontalContentAlignment="Left">
348357

App/Views/SettingsWindow.xaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<winuiex:WindowEx
4+
x:Class="Coder.Desktop.App.Views.SettingsWindow"
5+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
6+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
7+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
8+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
9+
xmlns:winuiex="using:WinUIEx"
10+
mc:Ignorable="d"
11+
Title="Coder Settings"
12+
Width="1000" Height="300"
13+
MinWidth="1000" MinHeight="300">
14+
15+
<Window.SystemBackdrop>
16+
<DesktopAcrylicBackdrop />
17+
</Window.SystemBackdrop>
18+
19+
<Frame x:Name="RootFrame" />
20+
</winuiex:WindowEx>

App/Views/SettingsWindow.xaml.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Coder.Desktop.App.Utils;
2+
using Coder.Desktop.App.ViewModels;
3+
using Coder.Desktop.App.Views.Pages;
4+
using Microsoft.UI.Xaml.Media;
5+
using WinUIEx;
6+
7+
namespace Coder.Desktop.App.Views;
8+
9+
public sealed partial class SettingsWindow : WindowEx
10+
{
11+
public readonly SettingsViewModel ViewModel;
12+
13+
public SettingsWindow(SettingsViewModel viewModel)
14+
{
15+
ViewModel = viewModel;
16+
InitializeComponent();
17+
TitleBarIcon.SetTitlebarIcon(this);
18+
19+
SystemBackdrop = new DesktopAcrylicBackdrop();
20+
21+
ViewModel.Initialize(this, DispatcherQueue);
22+
RootFrame.Content = new SettingsMainPage(ViewModel);
23+
24+
this.CenterOnScreen();
25+
}
26+
}

0 commit comments

Comments
 (0)