Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,17 @@ jobs:
with:
name: '*'

# Uploaded after the cleanup step above so a failed run keeps its diagnostics
- if: failure()
name: Upload the test diagnostics to the Artifacts
uses: actions/upload-artifact@v7
with:
name: 'Test Diagnostics (${{ env.CONFIGURATION }}, ${{ env.AUTOMATED_TESTS_ARCHITECTURE }})'
if-no-files-found: warn
path: |
${{ env.AUTOMATED_TESTS_ASSEMBLY_DIR }}\appium.log
${{ env.AUTOMATED_TESTS_ASSEMBLY_DIR }}\testResults.trx

# - name: Generate markdown from the tests result
# shell: pwsh
# run: |
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<PackageVersion Include="Uno.Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.4.2" />
<PackageVersion Include="UTF.Unknown" Version="2.7.0" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.3.1" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.335" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.346" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.9.0" />
<PackageVersion Include="PolySharp" Version="1.16.0" />
Expand Down
6 changes: 3 additions & 3 deletions src/Files.App/Actions/Navigation/NextTabAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ public async Task ExecuteAsync(object? parameter = null)
// Small delay for the UI to load
await Task.Delay(500);

// Focus the content of the selected tab item (needed for keyboard navigation)
var paneHolder = contentPageContext.ShellPage.GetRequiredPaneHolder();
paneHolder.FocusActivePane();
// Focus the content of the selected tab item (needed for keyboard navigation).
// The tab can be gone by the time the delay elapses, and focusing it is best effort
contentPageContext.ShellPage?.PaneHolder?.FocusActivePane();
}

private void MultitaskingContext_PropertyChanged(object? sender, PropertyChangedEventArgs e)
Expand Down
6 changes: 3 additions & 3 deletions src/Files.App/Actions/Navigation/PreviousTabAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ public async Task ExecuteAsync(object? parameter = null)
// Small delay for the UI to load
await Task.Delay(500);

// Focus the content of the selected tab item (needed for keyboard navigation)
var paneHolder = contentPageContext.ShellPage.GetRequiredPaneHolder();
paneHolder.FocusActivePane();
// Focus the content of the selected tab item (needed for keyboard navigation).
// The tab can be gone by the time the delay elapses, and focusing it is best effort
contentPageContext.ShellPage?.PaneHolder?.FocusActivePane();
}

private void MultitaskingContext_PropertyChanged(object? sender, PropertyChangedEventArgs e)
Expand Down
86 changes: 41 additions & 45 deletions src/Files.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ async Task ActivateAsync()
{
// Build the DI container off-thread while the window initializes
var appModel = AppModel;
Task commandsWarmupTask = Task.CompletedTask;
var servicesTask = Task.Run(() =>
{
try
Expand All @@ -108,31 +109,36 @@ async Task ActivateAsync()
// Configure Ioc here so Ioc.Default-dependent constructions warm off-thread too
Ioc.Default.ConfigureServices(provider);

// Warm the settings file reads off the UI thread
// Load the appearance settings needed for the first frame off the UI thread.
_ = provider.GetRequiredService<IGeneralSettingsService>().LeaveAppRunning;
_ = provider.GetRequiredService<IAppearanceSettingsService>().AppThemeBackdropMaterial;

// Read through these statics by the action/context ctors warmed below
QuickAccessManager = provider.GetRequiredService<QuickAccessManager>();
HistoryWrapper = provider.GetRequiredService<StorageHistoryWrapper>();
FileTagsManager = provider.GetRequiredService<FileTagsManager>();
LibraryManager = provider.GetRequiredService<LibraryManager>();

// Warm every command and hotkey off-thread, below normal so window creation wins the cores
var previousPriority = Thread.CurrentThread.Priority;
Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
try
{
_ = provider.GetRequiredService<ICommandManager>();
}
catch (Exception)
{
// A command ctor that needs the UI thread aborts the warm-up; it runs on first use instead
}
finally
var appearanceSettings = provider.GetRequiredService<IAppearanceSettingsService>();
_ = appearanceSettings.AppThemeMode;
_ = appearanceSettings.AppThemeBackdropMaterial;

// Commands are needed for the main page, not for the initial window and backdrop.
commandsWarmupTask = Task.Run(() =>
{
Thread.CurrentThread.Priority = previousPriority;
}
var previousPriority = Thread.CurrentThread.Priority;
Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
try
{
// Action and context constructors access these statics.
QuickAccessManager = provider.GetRequiredService<QuickAccessManager>();
HistoryWrapper = provider.GetRequiredService<StorageHistoryWrapper>();
FileTagsManager = provider.GetRequiredService<FileTagsManager>();
LibraryManager = provider.GetRequiredService<LibraryManager>();

_ = provider.GetRequiredService<ICommandManager>();
}
catch (Exception)
{
// UI-thread-only constructors run on first use instead.
}
finally
{
Thread.CurrentThread.Priority = previousPriority;
}
});

return provider;
}
Expand All @@ -152,17 +158,8 @@ async Task ActivateAsync()

if (!isStartupTask)
{
// Initialize and activate MainWindow
MainWindow.Instance.Activate();

if (showSplashScreen)
{
// Wait for the Window to initialize
await Task.Delay(10);

SplashScreenLoadingTCS = new TaskCompletionSource();
MainWindow.Instance.ShowSplashScreen();
}
// Construct the window while appearance settings are loaded.
_ = MainWindow.Instance;
}

// Configure the DI (dependency injection) container
Expand All @@ -173,28 +170,27 @@ async Task ActivateAsync()
Ioc.Default.ConfigureServices(serviceProvider);
}

// Configure Sentry
if (AppLifecycleHelper.AppEnvironment is not AppEnvironment.Dev)
AppLifecycleHelper.ConfigureSentry();

var userSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();
var isLeaveAppRunning = userSettingsService.GeneralSettingsService.LeaveAppRunning;

if (isStartupTask && !isLeaveAppRunning)
if (!isStartupTask || !isLeaveAppRunning)
{
// Initialize and activate MainWindow
MainWindow.Instance.Activate();

if (showSplashScreen)
{
// Wait for the Window to initialize
await Task.Delay(10);

SplashScreenLoadingTCS = new TaskCompletionSource();
MainWindow.Instance.ShowSplashScreen();
}

if (!await MainWindow.Instance.ActivateWithBackdropAsync())
return;
}

await commandsWarmupTask;

// Configure Sentry
if (AppLifecycleHelper.AppEnvironment is not AppEnvironment.Dev)
AppLifecycleHelper.ConfigureSentry();

// TODO: Replace with DI
QuickAccessManager = Ioc.Default.GetRequiredService<QuickAccessManager>();
HistoryWrapper = Ioc.Default.GetRequiredService<StorageHistoryWrapper>();
Expand Down
30 changes: 24 additions & 6 deletions src/Files.App/Data/Items/WidgetFolderCardItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Win32;
using Windows.Win32.System.Com;
using Windows.Win32.System.WinRT;
using Windows.Win32.UI.Shell;

namespace Files.App.Data.Items
Expand All @@ -23,16 +25,17 @@ public sealed partial class WidgetFolderCardItem : WidgetCardItem, IWidgetCardIt

private BitmapImage? _Thumbnail;
public BitmapImage? Thumbnail { get => _Thumbnail; set => SetProperty(ref _Thumbnail, value); }
private bool _isDisposed;

// Constructor

public WidgetFolderCardItem(IWindowsStorable item, string text, bool isPinned, string tooltip)
public WidgetFolderCardItem(IWindowsStorable item, string text, string path, bool isPinned, string tooltip)
{
AutomationProperties = text;
Item = item;
Text = text;
IsPinned = isPinned;
Path = item.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING);
Path = path;
Tooltip = tooltip;
}

Expand All @@ -55,7 +58,7 @@ public WidgetFolderCardItem(string path, string text, bool isPinned, string tool

public async Task LoadCardThumbnailAsync()
{
if (string.IsNullOrEmpty(Path))
if (_isDisposed || string.IsNullOrEmpty(Path))
return;

if (Item is null)
Expand All @@ -71,15 +74,30 @@ public async Task LoadCardThumbnailAsync()
var thumbnailSize = (int)(Constants.ShellIconSizes.Large * App.AppModel.AppWindowDPI);
// Ensure thumbnail size is at least 1 to prevent layout errors
thumbnailSize = Math.Max(1, thumbnailSize);
Item.TryGetThumbnail(thumbnailSize, SIIGBF.SIIGBF_ICONONLY, out var rawThumbnailData);
if (rawThumbnailData is null)
var hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, Item.ThisPtr, out IAgileReference shellItemReference);
if (hr.ThrowIfFailedOnDebug().Failed)
return;

var rawThumbnailData = await STATask.RunPooled(() =>
{
if (shellItemReference.Resolve(out IShellItem shellItem).ThrowIfFailedOnDebug().Failed)
return null;

using var folder = new WindowsFolder(shellItem);
folder.TryGetThumbnail(thumbnailSize, SIIGBF.SIIGBF_ICONONLY, out var data);
return data;
}, App.Logger);
if (_isDisposed || rawThumbnailData is null)
return;

Thumbnail = await rawThumbnailData.ToBitmapAsync();
var thumbnail = await rawThumbnailData.ToBitmapAsync();
if (!_isDisposed)
Thumbnail = thumbnail;
}

public void Dispose()
{
_isDisposed = true;
Item?.Dispose();
}
}
Expand Down
36 changes: 26 additions & 10 deletions src/Files.App/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,26 @@ public static string WithEnding(this string str, string ending)
return result;
}

private static readonly Lock resourceLookupLock = new();

private static readonly ConcurrentDictionary<string, string> cachedResources = new();

private static readonly Dictionary<string, string> abbreviations = new()
{
{ ByteSize.KiloByteSymbol, Strings.KiloByteSymbol.GetLocalizedResource() },
{ ByteSize.MegaByteSymbol, Strings.MegaByteSymbol.GetLocalizedResource() },
{ ByteSize.GigaByteSymbol, Strings.GigaByteSymbol.GetLocalizedResource() },
{ ByteSize.TeraByteSymbol, Strings.TeraByteSymbol.GetLocalizedResource() },
{ ByteSize.PetaByteSymbol, Strings.PetaByteSymbol.GetLocalizedResource() },
{ ByteSize.BitSymbol, Strings.ByteSymbol.GetLocalizedResource() },
{ ByteSize.ByteSymbol, Strings.ByteSymbol.GetLocalizedResource() }
{ ByteSize.KiloByteSymbol, Strings.KiloByteSymbol },
{ ByteSize.MegaByteSymbol, Strings.MegaByteSymbol },
{ ByteSize.GigaByteSymbol, Strings.GigaByteSymbol },
{ ByteSize.TeraByteSymbol, Strings.TeraByteSymbol },
{ ByteSize.PetaByteSymbol, Strings.PetaByteSymbol },
{ ByteSize.BitSymbol, Strings.ByteSymbol },
{ ByteSize.ByteSymbol, Strings.ByteSymbol }
};

public static string ConvertSizeAbbreviation(this string value)
{
foreach (var item in abbreviations)
{
value = value.Replace(item.Key, item.Value, StringComparison.Ordinal);
value = value.Replace(item.Key, item.Value.GetLocalizedResource(), StringComparison.Ordinal);
}

return value;
Expand Down Expand Up @@ -99,9 +101,23 @@ public static string GetLocalizedResource(this string resourceKey)
return value;
}

value = ResourceLookup.TryGet(resourceKey);
lock (resourceLookupLock)
{
if (cachedResources.TryGetValue(resourceKey, out value))
return value;

value = ResourceLookup.TryGet(resourceKey);
return cachedResources[resourceKey] = value ?? string.Empty;
}
}

return cachedResources[resourceKey] = value ?? string.Empty;
internal static void ResetLocalizedResources()
{
lock (resourceLookupLock)
{
ResourceLookup.Reset();
cachedResources.Clear();
}
}
}
}
3 changes: 3 additions & 0 deletions src/Files.App/Helpers/Application/AppLanguageHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Globalization;
using Windows.Globalization;
using StringExtensions = Files.App.Extensions.StringExtensions;

namespace Files.App.Helpers
{
Expand Down Expand Up @@ -87,6 +88,7 @@ public static bool TryChange(int index)

// Update the primary language override
ApplicationLanguages.PrimaryLanguageOverride = index == 0 ? _defaultCode : PreferredLanguage.Code;
StringExtensions.ResetLocalizedResources();
return true;
}

Expand Down Expand Up @@ -117,6 +119,7 @@ public static bool TryChange(string code)

// Update the primary language override
ApplicationLanguages.PrimaryLanguageOverride = index == 0 ? _defaultCode : PreferredLanguage.Code;
StringExtensions.ResetLocalizedResources();
return true;
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/Files.App/Helpers/Application/AppLifecycleHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,15 @@ static AppLifecycleHelper()
/// <summary>
/// Gets application icon path.
/// </summary>
public static string AppIconPath { get; } =
SystemIO.Path.Combine(Package.Current.InstalledLocation.Path, AppEnvironment switch
public static string AppIconPath => _appIconPath.Value;

private static readonly Lazy<string> _appIconPath = new(() =>
SystemIO.Path.Combine(Package.Current.InstalledPath, AppEnvironment switch
{
AppEnvironment.Dev => Constants.AssetPaths.DevLogo,
AppEnvironment.SideloadPreview or AppEnvironment.StorePreview => Constants.AssetPaths.PreviewLogo,
_ => Constants.AssetPaths.StableLogo
});
}));

/// <summary>
/// Initializes the app components.
Expand Down
15 changes: 13 additions & 2 deletions src/Files.App/Helpers/ResourceLookup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,18 @@ namespace Files.App.Helpers
internal static class ResourceLookup
{
#if WINDOWS
private static readonly ResourceMap? _tree = new ResourceManager().MainResourceMap.TryGetSubtree("Resources");
private static readonly ResourceManager _manager = new();
private static readonly ResourceMap? _tree = _manager.MainResourceMap.TryGetSubtree("Resources");
private static ResourceContext? _context;

public static string? TryGet(string key) => _tree?.TryGetValue(key)?.ValueAsString;
public static string? TryGet(string key)
{
_context ??= _manager.CreateResourceContext();
return _tree?.TryGetValue(key, _context)?.ValueAsString;
}

/// <summary>Drops the cached context so a changed language override is picked up.</summary>
public static void Reset() => _context = null;
#else
private static readonly ResourceLoader _loader = ResourceLoader.GetForViewIndependentUse();

Expand All @@ -26,6 +35,8 @@ internal static class ResourceLookup
var value = _loader.GetString(key);
return string.IsNullOrEmpty(value) ? null : value;
}

public static void Reset() { }
#endif
}
}
Loading
Loading