Skip to content

Commit bb7eef2

Browse files
committed
feat(themes): New Theme Selector
1 parent 674fd95 commit bb7eef2

5 files changed

Lines changed: 108 additions & 96 deletions

File tree

src/App.axaml.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.IO;
3+
using System.Linq;
34
using System.Net.Http;
45
using System.Text.Json;
56
using System.Threading.Tasks;
@@ -14,6 +15,7 @@
1415
using Avalonia.Platform;
1516
using Avalonia.Styling;
1617
using Avalonia.Threading;
18+
using SourceGit.Models;
1719

1820
namespace SourceGit
1921
{
@@ -117,17 +119,21 @@ app.Resources[finalLocaleKey] is not ResourceDictionary targetLocale ||
117119
app._activeLocale = targetLocale;
118120
}
119121

120-
public static void SetTheme(string theme, string themeOverridesFile)
122+
public static void SetTheme(ThemeOverrides theme)
121123
{
122124
if (Current is not App app)
123125
return;
124126

125-
if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase))
127+
string themeOverridesFile = string.Empty;
128+
129+
if (theme.Name.Equals("Light", StringComparison.OrdinalIgnoreCase))
126130
app.RequestedThemeVariant = ThemeVariant.Light;
127-
else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase))
131+
else if (theme.Name.Equals("Dark", StringComparison.OrdinalIgnoreCase))
128132
app.RequestedThemeVariant = ThemeVariant.Dark;
129-
else
133+
else if (theme.Name.Equals("Default", StringComparison.OrdinalIgnoreCase))
130134
app.RequestedThemeVariant = ThemeVariant.Default;
135+
else
136+
themeOverridesFile = theme.FilePath;
131137

132138
if (app._themeOverrides != null)
133139
{
@@ -243,7 +249,7 @@ public override void Initialize()
243249

244250
var pref = ViewModels.Preferences.Instance;
245251
SetLocale(pref.Locale);
246-
SetTheme(pref.Theme, pref.ThemeOverrides);
252+
SetTheme(pref.Themes.FirstOrDefault(m=> m.Name == pref.Theme) ?? new ThemeOverrides("Default"));
247253
SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily);
248254
}
249255

src/Models/ThemeOverrides.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,23 @@ namespace SourceGit.Models
55
{
66
public class ThemeOverrides
77
{
8+
public ThemeOverrides()
9+
{
10+
11+
}
12+
13+
public ThemeOverrides(string name)
14+
{
15+
Name = name;
16+
}
17+
public string Version { get; set; }
18+
public string Name { get; set; }
19+
public string Author { get; set; }
20+
public string Url { get; set; }
821
public Dictionary<string, Color> BasicColors { get; set; } = new Dictionary<string, Color>();
922
public double GraphPenThickness { get; set; } = 2;
1023
public double OpacityForNotMergedCommits { get; set; } = 0.5;
1124
public List<Color> GraphColors { get; set; } = new List<Color>();
25+
public string FilePath { get; set; }
1226
}
1327
}

src/ViewModels/Preferences.cs

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
34
using System.IO;
5+
using System.Linq;
46
using System.Text.Json;
57
using System.Text.Json.Serialization;
68
using System.Threading.Tasks;
79
using Avalonia.Collections;
810
using CommunityToolkit.Mvvm.ComponentModel;
11+
using SourceGit.Models;
912

1013
namespace SourceGit.ViewModels
1114
{
@@ -41,26 +44,33 @@ public string Locale
4144
}
4245
}
4346

44-
public string Theme
47+
[JsonIgnore]
48+
public List<ThemeOverrides> Themes
4549
{
46-
get => _theme;
50+
get => _themes;
4751
set
4852
{
49-
if (SetProperty(ref _theme, value) && !_isLoading)
50-
App.SetTheme(_theme, _themeOverrides);
53+
SetProperty(ref _themes, value);
5154
}
5255
}
5356

54-
public string ThemeOverrides
57+
[JsonIgnore]
58+
public ThemeOverrides SelectedTheme
5559
{
56-
get => _themeOverrides;
60+
get
61+
{
62+
return Themes.FirstOrDefault(m => m.Name == Theme) ?? new ThemeOverrides("Default");
63+
}
5764
set
5865
{
59-
if (SetProperty(ref _themeOverrides, value) && !_isLoading)
60-
App.SetTheme(_theme, value);
66+
if (SetProperty(ref _selectedTheme, value) && !_isLoading)
67+
App.SetTheme(_selectedTheme);
68+
Theme = _selectedTheme.Name;
6169
}
6270
}
63-
71+
72+
public string Theme { get; set; }
73+
6474
public string DefaultFontFamily
6575
{
6676
get => _defaultFontFamily;
@@ -566,10 +576,7 @@ public RepositoryNode FindOrAddNodeByRepositoryPath(string repo, RepositoryNode
566576
{
567577
node = new RepositoryNode()
568578
{
569-
Id = normalized,
570-
Name = Path.GetFileName(normalized),
571-
Bookmark = 0,
572-
IsRepository = true,
579+
Id = normalized, Name = Path.GetFileName(normalized), Bookmark = 0, IsRepository = true,
573580
};
574581

575582
AddNode(node, parent, save);
@@ -649,6 +656,25 @@ public void Save()
649656

650657
private static Preferences Load()
651658
{
659+
try
660+
{
661+
var themesPath = Path.Combine(Native.OS.DataDir, "themes");
662+
var themeFiles = Directory.GetFiles(themesPath, "theme.json", SearchOption.AllDirectories);
663+
foreach (var file in themeFiles)
664+
{
665+
using var stream = File.OpenRead(file);
666+
var theme = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.ThemeOverrides);
667+
if (theme == null)
668+
continue;
669+
theme.FilePath = file;
670+
_themes.Add(theme);
671+
}
672+
}
673+
catch
674+
{
675+
// ignored
676+
}
677+
652678
var path = Path.Combine(Native.OS.DataDir, "preference.json");
653679
if (!File.Exists(path))
654680
return new Preferences();
@@ -791,7 +817,8 @@ private bool RemoveInvalidRepositoriesRecursive(List<RepositoryNode> collection)
791817
private bool _isLoading = true;
792818
private bool _isReadonly = true;
793819
private string _locale = "en_US";
794-
private string _theme = "Default";
820+
private ThemeOverrides _selectedTheme = new("Default");
821+
private static List<ThemeOverrides> _themes = new() { new ThemeOverrides("Default"), new ThemeOverrides("Dark"), new ThemeOverrides("Light") };
795822
private string _themeOverrides = string.Empty;
796823
private string _defaultFontFamily = string.Empty;
797824
private string _monospaceFontFamily = string.Empty;

src/Views/Preferences.axaml

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -193,19 +193,31 @@
193193
Text="{DynamicResource Text.Preferences.Appearance.Theme}"
194194
HorizontalAlignment="Right"
195195
Margin="0,0,16,0"/>
196-
<ComboBox Grid.Row="0" Grid.Column="1"
197-
MinHeight="28"
198-
Padding="8,0"
199-
HorizontalAlignment="Stretch"
200-
DisplayMemberBinding="{Binding Key, x:DataType=ThemeVariant}"
201-
SelectedItem="{Binding Theme, Mode=TwoWay, Converter={x:Static c:StringConverters.ToTheme}}">
202-
<ComboBox.Items>
203-
<ThemeVariant>Default</ThemeVariant>
204-
<ThemeVariant>Dark</ThemeVariant>
205-
<ThemeVariant>Light</ThemeVariant>
206-
</ComboBox.Items>
207-
</ComboBox>
208-
196+
<Grid Grid.Row="0" Grid.Column="1">
197+
<Grid.ColumnDefinitions>
198+
<ColumnDefinition Width="*" />
199+
<ColumnDefinition Width="28" />
200+
<ColumnDefinition Width="28" />
201+
</Grid.ColumnDefinitions>
202+
<ComboBox Grid.Column="0" Grid.Row="0" MinHeight="28"
203+
HorizontalContentAlignment="Stretch" HorizontalAlignment="Stretch"
204+
Padding="8,0"
205+
ItemsSource="{Binding Themes, Mode=OneWay}"
206+
SelectedItem="{Binding SelectedTheme, Mode=TwoWay}">
207+
<ComboBox.ItemTemplate>
208+
<DataTemplate x:DataType="{x:Type m:ThemeOverrides}">
209+
<TextBlock Text="{Binding Name, Mode=OneWay}"/>
210+
</DataTemplate>
211+
</ComboBox.ItemTemplate>
212+
</ComboBox>
213+
<Button Grid.Column="1" Grid.Row="0" Classes="icon_button" Width="28" Height="28" Click="OpenThemeRepository">
214+
<Path Width="14" Height="14" Data="{StaticResource Icons.Remotes}" Fill="{DynamicResource Brush.FG1}"/>
215+
</Button>
216+
<Button Grid.Column="2" Grid.Row="0" Classes="icon_button" Width="28" Height="28" Click="OpenThemeFolder">
217+
<Path Width="14" Height="14" Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>
218+
</Button>
219+
</Grid>
220+
209221
<TextBlock Grid.Row="1" Grid.Column="0"
210222
Text="{DynamicResource Text.Preferences.Appearance.DefaultFont}"
211223
HorizontalAlignment="Right"
@@ -270,27 +282,6 @@
270282
Value="{Binding EditorTabWidth, Mode=TwoWay}"/>
271283
</Grid>
272284

273-
<TextBlock Grid.Row="5" Grid.Column="0"
274-
Text="{DynamicResource Text.Preferences.Appearance.ThemeOverrides}"
275-
HorizontalAlignment="Right"
276-
Margin="0,0,16,0"/>
277-
<TextBox Grid.Row="5" Grid.Column="1"
278-
Height="28"
279-
CornerRadius="3"
280-
Text="{Binding ThemeOverrides, Mode=TwoWay}">
281-
<TextBox.InnerRightContent>
282-
<StackPanel Orientation="Horizontal">
283-
<Button Classes="icon_button" Width="28" Height="28" Click="SelectThemeOverrideFile">
284-
<Path Width="16" Height="16" Data="{StaticResource Icons.Folder.Open}" Margin="0,2,0,0" Fill="{DynamicResource Brush.FG1}"/>
285-
</Button>
286-
287-
<Button Classes="icon_button" Width="28" Height="28" Click="OpenThemeRepository">
288-
<Path Width="14" Height="14" Data="{StaticResource Icons.Remotes}" Fill="{DynamicResource Brush.FG1}"/>
289-
</Button>
290-
</StackPanel>
291-
</TextBox.InnerRightContent>
292-
</TextBox>
293-
294285
<CheckBox Grid.Row="6" Grid.Column="1"
295286
Height="32"
296287
Content="{DynamicResource Text.Preferences.Appearance.UseFixedTabWidth}"

src/Views/Preferences.axaml.cs

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.IO;
34
using System.Threading.Tasks;
4-
55
using Avalonia;
66
using Avalonia.Controls;
77
using Avalonia.Interactivity;
@@ -226,42 +226,32 @@ protected override async void OnClosing(WindowClosingEventArgs e)
226226
preferences.Save();
227227
}
228228

229-
private async void SelectThemeOverrideFile(object _, RoutedEventArgs e)
229+
private void OpenThemeRepository(object _, RoutedEventArgs e)
230230
{
231-
var options = new FilePickerOpenOptions()
232-
{
233-
FileTypeFilter = [new FilePickerFileType("Theme Overrides File") { Patterns = ["*.json"] }],
234-
AllowMultiple = false,
235-
};
231+
Native.OS.OpenBrowser($"https://github.com/sourcegit-scm/sourcegit-theme");
232+
e.Handled = true;
233+
}
236234

235+
private async void OpenThemeFolder(object _, RoutedEventArgs e)
236+
{
237+
var themesPath = Path.Combine(Native.OS.DataDir, "themes");
237238
try
238239
{
239-
var selected = await StorageProvider.OpenFilePickerAsync(options);
240-
if (selected is { Count: 1 })
241-
ViewModels.Preferences.Instance.ThemeOverrides = selected[0].Path.LocalPath;
240+
if (!Directory.Exists(themesPath)) Directory.CreateDirectory(themesPath);
241+
Native.OS.OpenInFileManager(themesPath);
242242
}
243-
catch (Exception ex)
243+
catch
244244
{
245-
await new Alert().ShowAsync(this, $"Failed to select theme override file: {ex.Message}", true);
245+
// Ignore
246246
}
247247

248248
e.Handled = true;
249249
}
250250

251-
private void OpenThemeRepository(object _, RoutedEventArgs e)
252-
{
253-
Native.OS.OpenBrowser($"https://github.com/sourcegit-scm/sourcegit-theme");
254-
e.Handled = true;
255-
}
256-
257251
private async void SelectGitExecutable(object _, RoutedEventArgs e)
258252
{
259253
var pattern = OperatingSystem.IsWindows() ? "git.exe" : "git";
260-
var options = new FilePickerOpenOptions()
261-
{
262-
FileTypeFilter = [new FilePickerFileType("Git Executable") { Patterns = [pattern] }],
263-
AllowMultiple = false,
264-
};
254+
var options = new FilePickerOpenOptions() { FileTypeFilter = [new FilePickerFileType("Git Executable") { Patterns = [pattern] }], AllowMultiple = false, };
265255

266256
try
267257
{
@@ -309,11 +299,7 @@ private async void SelectGPGExecutable(object _, RoutedEventArgs e)
309299
else
310300
patterns.Add(GPGFormat.Program);
311301

312-
var options = new FilePickerOpenOptions()
313-
{
314-
FileTypeFilter = [new FilePickerFileType("GPG Program") { Patterns = patterns }],
315-
AllowMultiple = false,
316-
};
302+
var options = new FilePickerOpenOptions() { FileTypeFilter = [new FilePickerFileType("GPG Program") { Patterns = patterns }], AllowMultiple = false, };
317303

318304
try
319305
{
@@ -339,11 +325,7 @@ private async void SelectShellOrTerminal(object _, RoutedEventArgs e)
339325
var options = new FilePickerOpenOptions() { AllowMultiple = false };
340326
if (shell.Type != "custom")
341327
{
342-
options = new FilePickerOpenOptions()
343-
{
344-
FileTypeFilter = [new FilePickerFileType(shell.Name) { Patterns = [shell.Exec] }],
345-
AllowMultiple = false,
346-
};
328+
options = new FilePickerOpenOptions() { FileTypeFilter = [new FilePickerFileType(shell.Name) { Patterns = [shell.Exec] }], AllowMultiple = false, };
347329
}
348330

349331
try
@@ -371,11 +353,7 @@ private async void SelectExternalMergeTool(object _, RoutedEventArgs e)
371353
}
372354

373355
var tool = Models.ExternalMerger.Supported[type];
374-
var options = new FilePickerOpenOptions()
375-
{
376-
FileTypeFilter = [new FilePickerFileType(tool.Name) { Patterns = tool.GetPatternsToFindExecFile() }],
377-
AllowMultiple = false,
378-
};
356+
var options = new FilePickerOpenOptions() { FileTypeFilter = [new FilePickerFileType(tool.Name) { Patterns = tool.GetPatternsToFindExecFile() }], AllowMultiple = false, };
379357

380358
try
381359
{
@@ -449,11 +427,7 @@ private void OnAddCustomAction(object sender, RoutedEventArgs e)
449427

450428
private async void SelectExecutableForCustomAction(object sender, RoutedEventArgs e)
451429
{
452-
var options = new FilePickerOpenOptions()
453-
{
454-
AllowMultiple = false,
455-
FileTypeFilter = [new("Executable file(script)") { Patterns = ["*"] }]
456-
};
430+
var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new("Executable file(script)") { Patterns = ["*"] }] };
457431

458432
try
459433
{

0 commit comments

Comments
 (0)