Skip to content
Open
2 changes: 1 addition & 1 deletion .github/actions/spell-check/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,6 @@ lowlevel
LOWORD
lparam
LPBITMAPINFOHEADER
LPCFHOOKPROC
lpch
LPCITEMIDLIST
LPCLSID
Expand Down Expand Up @@ -1442,6 +1441,7 @@ SKEXP
SKIPOWNPROCESS
sku
SLGP
slideshow
sln
SMALLICON
smartphone
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ public ColorizationMode ColorizationMode
OnPropertyChanged(nameof(IsImageTintIntensityVisible));
OnPropertyChanged(nameof(EffectiveTintIntensity));
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
OnPropertyChanged(nameof(IsSingleImageBackgroundControlsVisible));
OnPropertyChanged(nameof(IsSlideshowBackgroundControlsVisible));
Comment thread Fixed
OnPropertyChanged(nameof(IsBackgroundSlideshowControlsVisible));
Comment thread Fixed
OnPropertyChanged(nameof(IsNoBackgroundVisible));
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
OnPropertyChanged(nameof(IsResetButtonVisible));
Expand Down Expand Up @@ -211,6 +214,7 @@ public string BackgroundImagePath
{
_settingsService.UpdateSettings(s => s with { BackgroundImagePath = value });
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));

if (BackgroundImageOpacity == 0)
{
Expand Down Expand Up @@ -298,6 +302,78 @@ public int BackgroundImageFitIndex
};
}

public string BackgroundImageSlideshowFolderPath
Comment thread Fixed
{
get => _settingsService.Settings.BackgroundImageSlideshowFolderPath ?? string.Empty;
Comment thread Fixed
set
{
if (_settingsService.Settings.BackgroundImageSlideshowFolderPath != value)
Comment thread Fixed
{
_settingsService.Settings.BackgroundImageSlideshowFolderPath = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsBackgroundSlideshowControlsVisible));
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));

if (BackgroundImageOpacity == 0)
{
BackgroundImageOpacity = 100;
}

_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}
}
}

public int BackgroundImageChangeIntervalMinutes
{
get => Math.Max(_settingsService.Settings.BackgroundImageChangeIntervalMinutes, 0);
set
{
var normalized = Math.Max(value, 0);
if (_settingsService.Settings.BackgroundImageChangeIntervalMinutes != normalized)
{
_settingsService.Settings.BackgroundImageChangeIntervalMinutes = normalized;
OnPropertyChanged();
OnPropertyChanged(nameof(BackgroundImageChangeIntervalIndex));
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}
}
}

public int BackgroundImageChangeIntervalIndex
{
get => BackgroundImageChangeIntervalMinutes switch
{
1 => 1,
10 => 2,
30 => 3,
60 => 4,
_ => 0,
};
set => BackgroundImageChangeIntervalMinutes = value switch
{
1 => 1,
2 => 10,
3 => 30,
4 => 60,
_ => 0,
};
}

public bool BackgroundImageShuffle
{
get => _settingsService.Settings.BackgroundImageShuffle;
set
{
if (_settingsService.Settings.BackgroundImageShuffle != value)
{
_settingsService.Settings.BackgroundImageShuffle = value;
OnPropertyChanged();
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}
}
}

public int BackdropOpacity
{
get => _settingsService.Settings.BackdropOpacity;
Expand Down Expand Up @@ -388,26 +464,35 @@ EffectiveBackdropStyle is not null
[ObservableProperty]
public partial bool IsColorizationDetailsExpanded { get; set; }

public bool IsCustomTintVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsCustomTintVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image or ColorizationMode.Slideshow;

public bool IsColorIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;

public bool IsImageTintIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
public bool IsImageTintIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image or ColorizationMode.Slideshow;

/// <summary>
/// Gets the effective tint intensity for the preview, based on the current colorization mode.
/// </summary>
public int EffectiveTintIntensity => _settingsService.Settings.ColorizationMode is ColorizationMode.Image
public int EffectiveTintIntensity => _settingsService.Settings.ColorizationMode is ColorizationMode.Image or ColorizationMode.Slideshow
? _settingsService.Settings.BackgroundImageTintIntensity
: _settingsService.Settings.CustomThemeColorIntensity;

public bool IsBackgroundControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
public bool IsBackgroundControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image or ColorizationMode.Slideshow;

public bool IsSingleImageBackgroundControlsVisible =>
_settingsService.Settings.ColorizationMode is ColorizationMode.Image;

public bool IsSlideshowBackgroundControlsVisible =>
_settingsService.Settings.ColorizationMode is ColorizationMode.Slideshow;

public bool IsBackgroundSlideshowControlsVisible =>
IsSlideshowBackgroundControlsVisible && BackgroundImagePathResolver.TryGetLocalFolderPath(BackgroundImageSlideshowFolderPath, out _);

public bool IsNoBackgroundVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.None;

public bool IsAccentColorControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.WindowsAccentColor;

public bool IsResetButtonVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
public bool IsResetButtonVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image or ColorizationMode.Slideshow;

public BackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);

Expand All @@ -419,7 +504,7 @@ EffectiveBackdropStyle is not null
: ColorizationMode switch
{
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
ColorizationMode.CustomColor or ColorizationMode.Image or ColorizationMode.Slideshow => ThemeColor,
_ => Colors.Transparent,
};

Expand All @@ -431,12 +516,20 @@ EffectiveBackdropStyle is not null
public ImageSource? EffectiveBackgroundImageSource =>
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsBackgroundImage
? null
: ColorizationMode is ColorizationMode.Image
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
: ColorizationMode is ColorizationMode.Image or ColorizationMode.Slideshow
&& BackgroundImagePathResolver.CreateImageUri(GetEffectiveBackgroundPreviewPath()) is Uri uri
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;

private string? GetEffectiveBackgroundPreviewPath()
{
return ColorizationMode switch
{
ColorizationMode.Slideshow => BackgroundImagePathResolver.ResolvePreviewImagePath(BackgroundImageSlideshowFolderPath),
_ => BackgroundImagePath,
};
Comment thread
Error0229 marked this conversation as resolved.
}

public AppearanceSettingsViewModel(IThemeService themeService, ISettingsService settingsService)
{
_themeService = themeService;
Expand Down Expand Up @@ -503,6 +596,8 @@ private void ResetBackgroundImageProperties()
BackgroundImageFit = BackgroundImageFit.UniformToFill;
BackgroundImageOpacity = 100;
BackgroundImageTintIntensity = 0;
BackgroundImageChangeIntervalMinutes = 0;
BackgroundImageShuffle = true;
}

[RelayCommand]
Expand All @@ -517,6 +612,7 @@ private void ResetAppearanceSettings()

// Reset background image settings
BackgroundImagePath = string.Empty;
BackgroundImageSlideshowFolderPath = string.Empty;
ResetBackgroundImageProperties();

// Reset colorization
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Frozen;

namespace Microsoft.CmdPal.UI.ViewModels;

/// <summary>
/// Resolves configured background image paths that can target either a single image file or a folder.
/// </summary>
public static class BackgroundImagePathResolver
{
public static readonly FrozenSet<string> SupportedImageExtensions = FrozenSet.ToFrozenSet(
new[]
{
".png",
".bmp",
".jpg",
".jpeg",
".jfif",
".gif",
".tiff",
".tif",
".webp",
".jxr",
},
StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Returns true when <paramref name="configuredPath"/> points to an existing local directory.
/// </summary>
public static bool TryGetLocalFolderPath(string? configuredPath, out string folderPath)
{
folderPath = string.Empty;
if (string.IsNullOrWhiteSpace(configuredPath))
{
return false;
}

var candidate = configuredPath.Trim();

if (Uri.TryCreate(candidate, UriKind.Absolute, out var absoluteUri))
{
if (!absoluteUri.IsFile)
{
return false;
}

var localPath = absoluteUri.LocalPath;
if (!Directory.Exists(localPath))
{
return false;
}

folderPath = localPath;
return true;
}

if (!Uri.TryCreate(candidate, UriKind.RelativeOrAbsolute, out var uri) || uri.IsAbsoluteUri)
{
return false;
}

try
{
var localCandidate = Path.GetFullPath(candidate);
if (!Directory.Exists(localCandidate))
{
return false;
}

folderPath = localCandidate;
return true;
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
{
return false;
}
}

/// <summary>
/// Gets sorted image files from <paramref name="folderPath"/> supported by the background pipeline.
/// </summary>
public static IReadOnlyList<string> GetSupportedImageFiles(string folderPath)
{
if (string.IsNullOrWhiteSpace(folderPath) || !Directory.Exists(folderPath))
{
return [];
}

try
{
return Directory
.EnumerateFiles(folderPath, "*", SearchOption.TopDirectoryOnly)
.Where(IsSupportedImageFile)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return [];
}
}

/// <summary>
/// Resolves a stable preview path for settings UI. For folders, this returns the first supported image.
/// </summary>
public static string? ResolvePreviewImagePath(string? configuredPath)
{
if (string.IsNullOrWhiteSpace(configuredPath))
{
return null;
}

if (!TryGetLocalFolderPath(configuredPath, out var folderPath))
{
return configuredPath.Trim();
}

var files = GetSupportedImageFiles(folderPath);
return files.Count > 0 ? files[0] : null;
}

/// <summary>
/// Creates a <see cref="Uri"/> suitable for <see cref="Microsoft.UI.Xaml.Media.Imaging.BitmapImage"/>
/// from a resolved image path. Returns null when the path is empty or invalid.
/// </summary>
public static Uri? CreateImageUri(string? imagePath)
{
if (string.IsNullOrWhiteSpace(imagePath))
{
return null;
}

if (!Uri.TryCreate(imagePath, UriKind.RelativeOrAbsolute, out var uri))
{
return null;
}

if (!uri.IsAbsoluteUri && File.Exists(imagePath))
{
return new Uri(Path.GetFullPath(imagePath));
}

return uri;
}

private static bool IsSupportedImageFile(string filePath)
{
var extension = Path.GetExtension(filePath);
return SupportedImageExtensions.Contains(extension);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public enum ColorizationMode
WindowsAccentColor,
CustomColor,
Image,
Slideshow,
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,7 @@ public int BackgroundImageFitIndex

public ImageSource? EffectiveBackgroundImageSource =>
ColorizationMode is ColorizationMode.Image
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
&& BackgroundImagePathResolver.CreateImageUri(BackgroundImagePathResolver.ResolvePreviewImagePath(BackgroundImagePath)) is Uri uri
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;

Expand Down
Loading
Loading