diff --git a/src/SyncTrayzor.Tests/ConfigurationThemeTests.cs b/src/SyncTrayzor.Tests/ConfigurationThemeTests.cs
new file mode 100644
index 00000000..63a36fa5
--- /dev/null
+++ b/src/SyncTrayzor.Tests/ConfigurationThemeTests.cs
@@ -0,0 +1,30 @@
+using SyncTrayzor.Services.Config;
+using SyncTrayzor.Services.Theming;
+using Xunit;
+
+namespace SyncTrayzor.Tests
+{
+ public class ConfigurationThemeTests
+ {
+ [Fact]
+ public void DefaultTheme_FollowsSystem()
+ {
+ var configuration = new Configuration();
+
+ Assert.Equal(ApplicationTheme.System, configuration.Theme);
+ }
+
+ [Theory]
+ [InlineData(ApplicationTheme.System)]
+ [InlineData(ApplicationTheme.Light)]
+ [InlineData(ApplicationTheme.Dark)]
+ public void CopyConstructor_PreservesTheme(ApplicationTheme theme)
+ {
+ var original = new Configuration { Theme = theme };
+
+ var copy = new Configuration(original);
+
+ Assert.Equal(theme, copy.Theme);
+ }
+ }
+}
diff --git a/src/SyncTrayzor/App.Installed.config b/src/SyncTrayzor/App.Installed.config
index b5d5eb8b..b5390b20 100644
--- a/src/SyncTrayzor/App.Installed.config
+++ b/src/SyncTrayzor/App.Installed.config
@@ -39,6 +39,7 @@
true
true
true
+ System
100
0
0
diff --git a/src/SyncTrayzor/App.Portable.config b/src/SyncTrayzor/App.Portable.config
index d682a752..b460dedc 100644
--- a/src/SyncTrayzor/App.Portable.config
+++ b/src/SyncTrayzor/App.Portable.config
@@ -39,6 +39,7 @@
true
true
true
+ System
100
0
0
diff --git a/src/SyncTrayzor/Bootstrapper.cs b/src/SyncTrayzor/Bootstrapper.cs
index 98d3e48b..0cc96e01 100644
--- a/src/SyncTrayzor/Bootstrapper.cs
+++ b/src/SyncTrayzor/Bootstrapper.cs
@@ -25,6 +25,7 @@
using System.Reflection;
using SyncTrayzor.Localization;
using SyncTrayzor.Services.Ipc;
+using SyncTrayzor.Services.Theming;
using System.Net;
using System.Runtime.Versioning;
using System.Windows.Media;
@@ -79,6 +80,7 @@ protected override void ConfigureIoC(IStyletIoCBuilder builder)
builder.Bind().To();
builder.Bind().To().InSingletonScope();
builder.Bind().To().InSingletonScope();
+ builder.Bind().To().InSingletonScope();
if (AppSettings.Instance.Variant == SyncTrayzorVariant.Installed)
builder.Bind().To();
@@ -397,4 +399,4 @@ public override void Dispose()
base.Dispose();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/SyncTrayzor/Pages/BarAlerts/BarAlertsView.xaml b/src/SyncTrayzor/Pages/BarAlerts/BarAlertsView.xaml
index 3022635c..65ba2312 100644
--- a/src/SyncTrayzor/Pages/BarAlerts/BarAlertsView.xaml
+++ b/src/SyncTrayzor/Pages/BarAlerts/BarAlertsView.xaml
@@ -11,7 +11,7 @@
-
diff --git a/src/SyncTrayzor/Pages/ConflictResolution/MultipleConflictsResolutionView.xaml b/src/SyncTrayzor/Pages/ConflictResolution/MultipleConflictsResolutionView.xaml
index 6151eb0c..28f0b2f2 100644
--- a/src/SyncTrayzor/Pages/ConflictResolution/MultipleConflictsResolutionView.xaml
+++ b/src/SyncTrayzor/Pages/ConflictResolution/MultipleConflictsResolutionView.xaml
@@ -23,7 +23,7 @@
-
@@ -55,6 +55,12 @@
+
+
+
+
ObfuscateDeviceIDs { get; }
public SettingItem UseComputerCulture { get; }
public SettingItem DisableHardwareRendering { get; }
+ public BindableCollection> ApplicationThemes { get; }
+ public SettingItem Theme { get; }
public SettingItem EnableConflictFileMonitoring { get; }
public SettingItem EnableFailedTransferAlerts { get; }
@@ -135,6 +138,13 @@ public SettingsViewModel(
UseComputerCulture.RequiresSyncTrayzorRestart = true;
DisableHardwareRendering = CreateBasicSettingItem(x => x.DisableHardwareRendering);
DisableHardwareRendering.RequiresSyncTrayzorRestart = true;
+ ApplicationThemes = new BindableCollection>()
+ {
+ LabelledValue.Create(Resources.SettingsView_Theme_System, ApplicationTheme.System),
+ LabelledValue.Create(Resources.SettingsView_Theme_Light, ApplicationTheme.Light),
+ LabelledValue.Create(Resources.SettingsView_Theme_Dark, ApplicationTheme.Dark),
+ };
+ Theme = CreateBasicSettingItem(x => x.Theme);
EnableConflictFileMonitoring = CreateBasicSettingItem(x => x.EnableConflictFileMonitoring);
EnableFailedTransferAlerts = CreateBasicSettingItem(x => x.EnableFailedTransferAlerts);
diff --git a/src/SyncTrayzor/Pages/Tray/FileTransfersTrayView.xaml b/src/SyncTrayzor/Pages/Tray/FileTransfersTrayView.xaml
index ff98fd68..0c63e360 100644
--- a/src/SyncTrayzor/Pages/Tray/FileTransfersTrayView.xaml
+++ b/src/SyncTrayzor/Pages/Tray/FileTransfersTrayView.xaml
@@ -14,7 +14,7 @@
d:DataContext="{Binding Source={StaticResource ViewModelLocator}, Path=FileTransfersTrayViewModel}">
@@ -25,12 +25,12 @@
-
+
-
+
@@ -54,7 +54,7 @@
-
@@ -66,7 +66,7 @@
-
+
@@ -75,7 +75,7 @@
-
@@ -103,7 +103,7 @@
-
+
@@ -135,12 +135,12 @@
-
+
-
diff --git a/src/SyncTrayzor/Pages/Tray/NetworkGraphView.xaml b/src/SyncTrayzor/Pages/Tray/NetworkGraphView.xaml
index b1a9d581..d8ab5664 100644
--- a/src/SyncTrayzor/Pages/Tray/NetworkGraphView.xaml
+++ b/src/SyncTrayzor/Pages/Tray/NetworkGraphView.xaml
@@ -12,7 +12,7 @@
Height="50"
Visibility="{Binding ShowGraph, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
-
+
diff --git a/src/SyncTrayzor/Properties/Resources.Designer.cs b/src/SyncTrayzor/Properties/Resources.Designer.cs
index aba76430..72ec9cf3 100644
--- a/src/SyncTrayzor/Properties/Resources.Designer.cs
+++ b/src/SyncTrayzor/Properties/Resources.Designer.cs
@@ -68,7 +68,7 @@ public static string AboutView_CreatedByAuthor {
return ResourceManager.GetString("AboutView_CreatedByAuthor", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to Donate now.
///
@@ -1173,6 +1173,30 @@ public static string SettingsView_Section_SyncTrayzor {
return ResourceManager.GetString("SettingsView_Section_SyncTrayzor", resourceCulture);
}
}
+
+ public static string SettingsView_Theme {
+ get {
+ return ResourceManager.GetString("SettingsView_Theme", resourceCulture);
+ }
+ }
+
+ public static string SettingsView_Theme_System {
+ get {
+ return ResourceManager.GetString("SettingsView_Theme_System", resourceCulture);
+ }
+ }
+
+ public static string SettingsView_Theme_Light {
+ get {
+ return ResourceManager.GetString("SettingsView_Theme_Light", resourceCulture);
+ }
+ }
+
+ public static string SettingsView_Theme_Dark {
+ get {
+ return ResourceManager.GetString("SettingsView_Theme_Dark", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to Tray Icon.
diff --git a/src/SyncTrayzor/Properties/Resources.resx b/src/SyncTrayzor/Properties/Resources.resx
index b9d3c241..439a0af6 100644
--- a/src/SyncTrayzor/Properties/Resources.resx
+++ b/src/SyncTrayzor/Properties/Resources.resx
@@ -250,6 +250,18 @@
SyncTrayzor
Section header for settings to do with configuring SyncTrayzor
+
+ Theme:
+
+
+ Use system setting
+
+
+ Light
+
+
+ Dark
+
Start Minimized
Setting allowing the user to specify that, when SyncTrayzor is started automatically on login, it should start in the tray, rather than being visible to the user
diff --git a/src/SyncTrayzor/Properties/Resources.tr.resx b/src/SyncTrayzor/Properties/Resources.tr.resx
index 4685d4fb..8167ac71 100644
--- a/src/SyncTrayzor/Properties/Resources.tr.resx
+++ b/src/SyncTrayzor/Properties/Resources.tr.resx
@@ -166,6 +166,18 @@
SyncTrayzor
+
+ Tema:
+
+
+ Sistem ayarını kullan
+
+
+ Açık
+
+
+ Koyu
+
Simge Durumunda başlat
@@ -801,4 +813,4 @@ Syncthing'i şimdi yeniden başlatmak istiyor musunuz?
Etkinlik açılır penceresini en üstte tut
-
\ No newline at end of file
+
diff --git a/src/SyncTrayzor/Services/Config/Configuration.cs b/src/SyncTrayzor/Services/Config/Configuration.cs
index a0836d9a..cd01cb20 100644
--- a/src/SyncTrayzor/Services/Config/Configuration.cs
+++ b/src/SyncTrayzor/Services/Config/Configuration.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Serialization;
+using SyncTrayzor.Services.Theming;
namespace SyncTrayzor.Services.Config
{
@@ -75,6 +76,7 @@ public string LatestNotifiedVersionRaw
public string OpenFolderCommand { get; set; }
public string ShowFileInFolderCommand { get; set; }
public LogLevel LogLevel { get; set; }
+ public ApplicationTheme Theme { get; set; }
public Configuration()
{
@@ -114,6 +116,7 @@ public Configuration()
OpenFolderCommand = "explorer.exe \"{0}\"";
ShowFileInFolderCommand = "explorer.exe /select, \"{0}\"";
LogLevel = LogLevel.Info;
+ Theme = ApplicationTheme.System;
KeepActivityPopupOpen = false;
KeepActivityPopupOnTop = true;
ActivityPopupWidth = 300;
@@ -156,6 +159,7 @@ public Configuration(Configuration other)
OpenFolderCommand = other.OpenFolderCommand;
ShowFileInFolderCommand = other.ShowFileInFolderCommand;
LogLevel = other.LogLevel;
+ Theme = other.Theme;
KeepActivityPopupOpen = other.KeepActivityPopupOpen;
KeepActivityPopupOnTop = other.KeepActivityPopupOnTop;
ActivityPopupWidth = other.ActivityPopupWidth;
@@ -178,7 +182,7 @@ public override string ToString()
$"EnableConflictFileMonitoring={EnableConflictFileMonitoring} " +
$"ConflictResolverDeletesToRecycleBin={ConflictResolverDeletesToRecycleBin} PauseDevicesOnMeteredNetworks={PauseDevicesOnMeteredNetworks} " +
$"HaveDonated={HaveDonated} IconAnimationMode={IconAnimationMode} OpenFolderCommand={OpenFolderCommand} ShowFileInFolderCommand={ShowFileInFolderCommand}" +
- $"LogLevel={LogLevel}" +
+ $"LogLevel={LogLevel} Theme={Theme}" +
$"KeepActivityPopupOpen={KeepActivityPopupOpen}>" +
$"KeepActivityPopupOnTop={KeepActivityPopupOnTop}>" +
$"ActivityPopupWindowWidth={ActivityPopupWidth}>"+
diff --git a/src/SyncTrayzor/Services/ConfigurationApplicator.cs b/src/SyncTrayzor/Services/ConfigurationApplicator.cs
index 98235580..51b39972 100644
--- a/src/SyncTrayzor/Services/ConfigurationApplicator.cs
+++ b/src/SyncTrayzor/Services/ConfigurationApplicator.cs
@@ -7,6 +7,7 @@
using System.Linq;
using SyncTrayzor.Services.Metering;
using System.Collections.Generic;
+using SyncTrayzor.Services.Theming;
namespace SyncTrayzor.Services
{
@@ -24,6 +25,7 @@ public class ConfigurationApplicator : IDisposable
private readonly IAlertsManager alertsManager;
private readonly IMeteredNetworkManager meteredNetworkManager;
private readonly IPathTransformer pathTransformer;
+ private readonly IThemeManager themeManager;
public ConfigurationApplicator(
IConfigurationProvider configurationProvider,
@@ -36,7 +38,8 @@ public ConfigurationApplicator(
IConflictFileWatcher conflictFileWatcher,
IAlertsManager alertsManager,
IMeteredNetworkManager meteredNetworkManager,
- IPathTransformer pathTransformer)
+ IPathTransformer pathTransformer,
+ IThemeManager themeManager)
{
this.configurationProvider = configurationProvider;
this.configurationProvider.ConfigurationChanged += ConfigurationChanged;
@@ -51,6 +54,7 @@ public ConfigurationApplicator(
this.alertsManager = alertsManager;
this.meteredNetworkManager = meteredNetworkManager;
this.pathTransformer = pathTransformer;
+ this.themeManager = themeManager;
this.syncthingManager.Folders.FoldersChanged += FoldersChanged;
this.updateManager.VersionIgnored += VersionIgnored;
@@ -117,6 +121,8 @@ private void ApplyNewConfiguration(Configuration configuration)
alertsManager.EnableConflictedFileAlerts = configuration.EnableConflictFileMonitoring;
alertsManager.EnableFailedTransferAlerts = configuration.EnableFailedTransferAlerts;
+ themeManager.Apply(configuration.Theme);
+
SetLogLevel(configuration);
}
diff --git a/src/SyncTrayzor/Services/Theming/ApplicationTheme.cs b/src/SyncTrayzor/Services/Theming/ApplicationTheme.cs
new file mode 100644
index 00000000..ad973fed
--- /dev/null
+++ b/src/SyncTrayzor/Services/Theming/ApplicationTheme.cs
@@ -0,0 +1,9 @@
+namespace SyncTrayzor.Services.Theming
+{
+ public enum ApplicationTheme
+ {
+ System,
+ Light,
+ Dark,
+ }
+}
diff --git a/src/SyncTrayzor/Services/Theming/ThemeManager.cs b/src/SyncTrayzor/Services/Theming/ThemeManager.cs
new file mode 100644
index 00000000..eee5fb32
--- /dev/null
+++ b/src/SyncTrayzor/Services/Theming/ThemeManager.cs
@@ -0,0 +1,166 @@
+using Microsoft.Win32;
+using NLog;
+using System;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Interop;
+using System.Windows.Media;
+
+namespace SyncTrayzor.Services.Theming
+{
+ public interface IThemeManager : IDisposable
+ {
+ ApplicationTheme SelectedTheme { get; }
+ bool IsDarkTheme { get; }
+ void Apply(ApplicationTheme theme);
+ }
+
+ public sealed class ThemeManager : IThemeManager
+ {
+ private const string PersonalizeRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
+ private const string AppsUseLightThemeValue = "AppsUseLightTheme";
+ private const int DwmUseImmersiveDarkMode = 20;
+ private const int DwmUseImmersiveDarkModeBefore20H1 = 19;
+
+ private static readonly Logger logger = LogManager.GetCurrentClassLogger();
+
+ public ApplicationTheme SelectedTheme { get; private set; } = ApplicationTheme.System;
+ public bool IsDarkTheme { get; private set; }
+
+ public ThemeManager()
+ {
+ EventManager.RegisterClassHandler(
+ typeof(Window),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler(WindowLoaded));
+ SystemEvents.UserPreferenceChanged += UserPreferenceChanged;
+ }
+
+ public void Apply(ApplicationTheme theme)
+ {
+ if (!Application.Current.Dispatcher.CheckAccess())
+ {
+ Application.Current.Dispatcher.Invoke(() => Apply(theme));
+ return;
+ }
+
+ SelectedTheme = theme;
+ IsDarkTheme = theme == ApplicationTheme.Dark ||
+ (theme == ApplicationTheme.System && SystemUsesDarkTheme());
+
+ ApplyPalette(IsDarkTheme);
+
+ foreach (Window window in Application.Current.Windows)
+ ApplyWindowTheme(window);
+ }
+
+ private static void ApplyPalette(bool dark)
+ {
+ var resources = Application.Current.Resources;
+
+ var background = ColorFrom(dark ? "#1E1E1E" : "#FFFFFF");
+ var surface = ColorFrom(dark ? "#252526" : "#F5F5F5");
+ var control = ColorFrom(dark ? "#2D2D30" : "#FFFFFF");
+ var foreground = ColorFrom(dark ? "#F3F3F3" : "#1B1B1B");
+ var mutedForeground = ColorFrom(dark ? "#B8B8B8" : "#606060");
+ var border = ColorFrom(dark ? "#4A4A4A" : "#B8B8B8");
+ var accent = ColorFrom(dark ? "#3EA6E8" : "#0078D4");
+ var focusBorder = ColorFrom(dark ? "#707070" : "#686868");
+ var hover = ColorFrom(dark ? "#3A3A3D" : "#E7E7E7");
+ var pressed = ColorFrom(dark ? "#454549" : "#D8D8D8");
+ var disabled = ColorFrom(dark ? "#858585" : "#707070");
+ var scrollBarTrack = ColorFrom(dark ? "#252526" : "#E5E5E5");
+ var scrollBarThumb = ColorFrom(dark ? "#8A8A8A" : "#858585");
+ var scrollBarThumbHover = ColorFrom(dark ? "#A5A5A5" : "#686868");
+
+ SetBrush(resources, "AppBackgroundBrush", background);
+ SetBrush(resources, "AppSurfaceBrush", surface);
+ SetBrush(resources, "AppControlBackgroundBrush", control);
+ SetBrush(resources, "AppForegroundBrush", foreground);
+ SetBrush(resources, "AppMutedForegroundBrush", mutedForeground);
+ SetBrush(resources, "AppBorderBrush", border);
+ SetBrush(resources, "AppAccentBrush", accent);
+ SetBrush(resources, "AppFocusBorderBrush", focusBorder);
+ SetBrush(resources, "AppHoverBrush", hover);
+ SetBrush(resources, "AppPressedBrush", pressed);
+ SetBrush(resources, "AppScrollBarTrackBrush", scrollBarTrack);
+ SetBrush(resources, "AppScrollBarThumbBrush", scrollBarThumb);
+ SetBrush(resources, "AppScrollBarThumbHoverBrush", scrollBarThumbHover);
+
+ // Default WPF templates consume these system resource keys. Overriding them
+ // keeps controls which do not have a SyncTrayzor-specific style in theme.
+ SetBrush(resources, SystemColors.WindowBrushKey, background);
+ SetBrush(resources, SystemColors.WindowTextBrushKey, foreground);
+ SetBrush(resources, SystemColors.ControlBrushKey, control);
+ SetBrush(resources, SystemColors.ControlTextBrushKey, foreground);
+ SetBrush(resources, SystemColors.MenuBrushKey, surface);
+ SetBrush(resources, SystemColors.MenuTextBrushKey, foreground);
+ SetBrush(resources, SystemColors.HighlightBrushKey, accent);
+ SetBrush(resources, SystemColors.HighlightTextBrushKey, Colors.White);
+ SetBrush(resources, SystemColors.GrayTextBrushKey, disabled);
+ SetBrush(resources, SystemColors.ActiveBorderBrushKey, border);
+ SetBrush(resources, SystemColors.InactiveBorderBrushKey, border);
+ SetBrush(resources, SystemColors.InactiveSelectionHighlightBrushKey, hover);
+ SetBrush(resources, SystemColors.InactiveSelectionHighlightTextBrushKey, foreground);
+ }
+
+ private static void SetBrush(ResourceDictionary resources, object key, Color color)
+ {
+ resources[key] = new SolidColorBrush(color);
+ }
+
+ private static Color ColorFrom(string color)
+ {
+ return (Color)ColorConverter.ConvertFromString(color);
+ }
+
+ private void WindowLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is Window window)
+ ApplyWindowTheme(window);
+ }
+
+ private void UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
+ {
+ if (SelectedTheme == ApplicationTheme.System)
+ Apply(ApplicationTheme.System);
+ }
+
+ private void ApplyWindowTheme(Window window)
+ {
+ window.SetResourceReference(Control.BackgroundProperty, "AppBackgroundBrush");
+ window.SetResourceReference(Control.ForegroundProperty, "AppForegroundBrush");
+
+ var handle = new WindowInteropHelper(window).Handle;
+ if (handle == IntPtr.Zero)
+ return;
+
+ var enabled = IsDarkTheme ? 1 : 0;
+ if (DwmSetWindowAttribute(handle, DwmUseImmersiveDarkMode, ref enabled, sizeof(int)) != 0)
+ DwmSetWindowAttribute(handle, DwmUseImmersiveDarkModeBefore20H1, ref enabled, sizeof(int));
+ }
+
+ private static bool SystemUsesDarkTheme()
+ {
+ try
+ {
+ using var key = Registry.CurrentUser.OpenSubKey(PersonalizeRegistryPath);
+ return key?.GetValue(AppsUseLightThemeValue) is int value && value == 0;
+ }
+ catch (Exception e)
+ {
+ logger.Warn(e, "Could not read the Windows application theme; using light theme");
+ return false;
+ }
+ }
+
+ public void Dispose()
+ {
+ SystemEvents.UserPreferenceChanged -= UserPreferenceChanged;
+ }
+
+ [DllImport("dwmapi.dll")]
+ private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int valueSize);
+ }
+}
diff --git a/src/SyncTrayzor/Xaml/BoringListView.xaml b/src/SyncTrayzor/Xaml/BoringListView.xaml
index 8a2d013f..53d6c377 100644
--- a/src/SyncTrayzor/Xaml/BoringListView.xaml
+++ b/src/SyncTrayzor/Xaml/BoringListView.xaml
@@ -208,10 +208,10 @@
-
\ No newline at end of file
+
diff --git a/src/SyncTrayzor/Xaml/PassiveListView.xaml b/src/SyncTrayzor/Xaml/PassiveListView.xaml
index b9a6723b..047c2b98 100644
--- a/src/SyncTrayzor/Xaml/PassiveListView.xaml
+++ b/src/SyncTrayzor/Xaml/PassiveListView.xaml
@@ -24,7 +24,7 @@
-
+
@@ -42,8 +42,8 @@
-
-
+
+
@@ -205,10 +205,10 @@
-
\ No newline at end of file
+
diff --git a/src/SyncTrayzor/Xaml/Resources.xaml b/src/SyncTrayzor/Xaml/Resources.xaml
index e76e0be4..9daebdcb 100644
--- a/src/SyncTrayzor/Xaml/Resources.xaml
+++ b/src/SyncTrayzor/Xaml/Resources.xaml
@@ -1,13 +1,578 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+