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 +