diff --git a/CollapseLauncher/Classes/Extension/ObservableCollectionExtension.cs b/CollapseLauncher/Classes/Extension/ObservableCollectionExtension.cs new file mode 100644 index 000000000..9e3f0f39b --- /dev/null +++ b/CollapseLauncher/Classes/Extension/ObservableCollectionExtension.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace CollapseLauncher.Extension +{ + /// + /// Provides extension methods for . + /// + /// The type of elements in the collection. + internal static class ObservableCollectionExtension + { + /// + /// Gets the backing list of the specified collection. + /// + /// The collection to get the backing list from. + /// A reference to the backing list of the collection. + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "items")] + internal static extern ref IList GetBackedCollectionList(Collection source); + + /// + /// Invokes the OnCountPropertyChanged method on the specified observable collection. + /// + /// The observable collection to invoke the method on. + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnCountPropertyChanged")] + internal static extern void OnCountPropertyChanged(ObservableCollection source); + + /// + /// Invokes the OnIndexerPropertyChanged method on the specified observable collection. + /// + /// The observable collection to invoke the method on. + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnIndexerPropertyChanged")] + internal static extern void OnIndexerPropertyChanged(ObservableCollection source); + + /// + /// Invokes the OnCollectionChanged method on the specified observable collection. + /// + /// The observable collection to invoke the method on. + /// The event arguments for the collection changed event. + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnCollectionChanged")] + internal static extern void OnCollectionChanged(ObservableCollection source, NotifyCollectionChangedEventArgs e); + + /// + /// Refreshes all events for the specified observable collection. + /// + /// The observable collection to invoke the method on. + internal static void RefreshAllEvents(ObservableCollection source) + { + OnCountPropertyChanged(source); + OnIndexerPropertyChanged(source); + OnCollectionChanged(source, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// + /// Removes a range of items from the specified observable collection quickly. + /// + /// The list of items to remove from the target collection. + /// The observable collection from which the items will be removed. + /// Thrown when the backing list of the target collection cannot be cast to a List{T}. + /// + /// This method directly manipulates the backing list of the observable collection to remove the specified items, + /// and then fires the necessary property changed and collection changed events to update any bindings. + /// + internal static void RemoveItemsFast(List sourceRange, ObservableCollection target) + { + // Get the backed list instance of the collection + List targetBackedList = GetBackedCollectionList(target) as List ?? throw new InvalidCastException(); + + // Get the count and iterate the reference of the T from the source range + ReadOnlySpan sourceRangeSpan = CollectionsMarshal.AsSpan(sourceRange); + int len = sourceRangeSpan.Length - 1; + for (; len >= 0; len--) + { + // Remove the reference of the item T from the target backed list + _ = targetBackedList.Remove(sourceRangeSpan[len]); + } + + // Fire the changes event + RefreshAllEvents(target); + } + } +} diff --git a/CollapseLauncher/Classes/Extension/UIElementExtensions.UnsafeAccessorExtensions.cs b/CollapseLauncher/Classes/Extension/UIElementExtensions.UnsafeAccessorExtensions.cs new file mode 100644 index 000000000..55ab1f3dc --- /dev/null +++ b/CollapseLauncher/Classes/Extension/UIElementExtensions.UnsafeAccessorExtensions.cs @@ -0,0 +1,32 @@ +using Microsoft.UI.Input; +using Microsoft.UI.Xaml; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Runtime.CompilerServices; + +#nullable enable +namespace CollapseLauncher.Extension +{ + internal static partial class UIElementExtensions + { + /// + /// Set the cursor for the element. + /// + /// The member of an element + /// The cursor you want to set. Use to choose the cursor you want to set. + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_ProtectedCursor")] + internal static extern void SetCursor(this UIElement element, InputCursor inputCursor); + + /// + /// Set the cursor for the element. + /// + /// The member of an element + /// The cursor you want to set. Use to choose the cursor you want to set. + internal static ref T WithCursor(this T element, InputCursor inputCursor) where T : UIElement + { + element.SetCursor(inputCursor); + return ref Unsafe.AsRef(ref element); + } + } +} diff --git a/CollapseLauncher/Classes/Extension/UIElementExtensions.cs b/CollapseLauncher/Classes/Extension/UIElementExtensions.cs index 3a9e860fe..7727673f7 100644 --- a/CollapseLauncher/Classes/Extension/UIElementExtensions.cs +++ b/CollapseLauncher/Classes/Extension/UIElementExtensions.cs @@ -18,6 +18,7 @@ using Windows.UI; using Windows.UI.Text; using Hi3Helper.SentryHelper; +using System.Collections.ObjectModel; namespace CollapseLauncher.Extension { @@ -28,27 +29,8 @@ internal class NavigationViewItemLocaleTextProperty public string LocalePropertyName { get; set; } } - internal static class UIElementExtensions + internal static partial class UIElementExtensions { - /// - /// Set the cursor for the element. - /// - /// The member of an element - /// The cursor you want to set. Use to choose the cursor you want to set. - [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_ProtectedCursor")] - internal static extern void SetCursor(this UIElement element, InputCursor inputCursor); - - /// - /// Set the cursor for the element. - /// - /// The member of an element - /// The cursor you want to set. Use to choose the cursor you want to set. - internal static ref T WithCursor(this T element, InputCursor inputCursor) where T : UIElement - { - element.SetCursor(inputCursor); - return ref Unsafe.AsRef(ref element); - } - #nullable enable /// /// Set the initial navigation view item's locale binding before getting set with diff --git a/CollapseLauncher/Classes/Helper/Loading/LoadingMessageHelper.cs b/CollapseLauncher/Classes/Helper/Loading/LoadingMessageHelper.cs index 7dd9c2aa0..7f68bceac 100644 --- a/CollapseLauncher/Classes/Helper/Loading/LoadingMessageHelper.cs +++ b/CollapseLauncher/Classes/Helper/Loading/LoadingMessageHelper.cs @@ -1,5 +1,6 @@ using CollapseLauncher.Extension; using CollapseLauncher.Helper.Animation; +using CommunityToolkit.WinUI; using CommunityToolkit.WinUI.Animations; using Microsoft.UI.Text; using Microsoft.UI.Xaml; @@ -64,28 +65,50 @@ internal static void SetProgressBarState(double maxValue = 100d, bool isProgress /// Show the loading frame. /// internal static async void ShowLoadingFrame() + { + if (currentMainWindow.LoadingStatusBackgroundGrid.DispatcherQueue.HasThreadAccess) + { + await ShowLoadingFrameInner(); + return; + } + + await currentMainWindow.LoadingStatusBackgroundGrid.DispatcherQueue.EnqueueAsync(ShowLoadingFrameInner); + } + + private static async Task ShowLoadingFrameInner() { if (isCurrentlyShow) return; - isCurrentlyShow = true; - currentMainWindow!.LoadingStatusGrid!.Visibility = Visibility.Visible; + isCurrentlyShow = true; + currentMainWindow!.LoadingStatusGrid!.Visibility = Visibility.Visible; currentMainWindow!.LoadingStatusBackgroundGrid!.Visibility = Visibility.Visible; TimeSpan duration = TimeSpan.FromSeconds(0.25); await Task.WhenAll( - AnimationHelper.StartAnimation(currentMainWindow.LoadingStatusBackgroundGrid, duration, - currentMainWindow.LoadingStatusBackgroundGrid.GetElementCompositor()!.CreateScalarKeyFrameAnimation("Opacity", 1, 0)), - AnimationHelper.StartAnimation(currentMainWindow.LoadingStatusGrid, duration, - currentMainWindow.LoadingStatusGrid.GetElementCompositor()!.CreateVector3KeyFrameAnimation("Translation", new Vector3(0,0,currentMainWindow.LoadingStatusGrid.Translation.Z), new Vector3(0, (float)(currentMainWindow.LoadingStatusGrid.ActualHeight + 16), currentMainWindow.LoadingStatusGrid.Translation.Z)), - currentMainWindow.LoadingStatusGrid.GetElementCompositor()!.CreateScalarKeyFrameAnimation("Opacity", 1, 0)) - ); + AnimationHelper.StartAnimation(currentMainWindow.LoadingStatusBackgroundGrid, duration, + currentMainWindow.LoadingStatusBackgroundGrid.GetElementCompositor()!.CreateScalarKeyFrameAnimation("Opacity", 1, 0)), + AnimationHelper.StartAnimation(currentMainWindow.LoadingStatusGrid, duration, + currentMainWindow.LoadingStatusGrid.GetElementCompositor()!.CreateVector3KeyFrameAnimation("Translation", new Vector3(0, 0, currentMainWindow.LoadingStatusGrid.Translation.Z), new Vector3(0, (float)(currentMainWindow.LoadingStatusGrid.ActualHeight + 16), currentMainWindow.LoadingStatusGrid.Translation.Z)), + currentMainWindow.LoadingStatusGrid.GetElementCompositor()!.CreateScalarKeyFrameAnimation("Opacity", 1, 0)) + ); } /// /// Hide the loading frame (also hide the action button). /// internal static async void HideLoadingFrame() + { + if (currentMainWindow.LoadingStatusBackgroundGrid.DispatcherQueue.HasThreadAccess) + { + await HideLoadingFrameInner(); + return; + } + + await currentMainWindow.LoadingStatusBackgroundGrid.DispatcherQueue.EnqueueAsync(HideLoadingFrameInner); + } + + private static async Task HideLoadingFrameInner() { if (!isCurrentlyShow) return; @@ -93,14 +116,14 @@ internal static async void HideLoadingFrame() TimeSpan duration = TimeSpan.FromSeconds(0.25); await Task.WhenAll( - AnimationHelper.StartAnimation(currentMainWindow.LoadingStatusBackgroundGrid, duration, - currentMainWindow.LoadingStatusBackgroundGrid.GetElementCompositor()!.CreateScalarKeyFrameAnimation("Opacity", 0, 1)), - AnimationHelper.StartAnimation(currentMainWindow.LoadingStatusGrid, duration, - currentMainWindow.LoadingStatusGrid.GetElementCompositor()!.CreateVector3KeyFrameAnimation("Translation", new Vector3(0, (float)(currentMainWindow.LoadingStatusGrid.ActualHeight + 16), currentMainWindow.LoadingStatusGrid.Translation.Z), new Vector3(0,0,currentMainWindow.LoadingStatusGrid.Translation.Z)), - currentMainWindow.LoadingStatusGrid.GetElementCompositor()!.CreateScalarKeyFrameAnimation("Opacity", 0, 1)) - ); - - currentMainWindow.LoadingStatusGrid.Visibility = Visibility.Collapsed; + AnimationHelper.StartAnimation(currentMainWindow.LoadingStatusBackgroundGrid, duration, + currentMainWindow.LoadingStatusBackgroundGrid.GetElementCompositor()!.CreateScalarKeyFrameAnimation("Opacity", 0, 1)), + AnimationHelper.StartAnimation(currentMainWindow.LoadingStatusGrid, duration, + currentMainWindow.LoadingStatusGrid.GetElementCompositor()!.CreateVector3KeyFrameAnimation("Translation", new Vector3(0, (float)(currentMainWindow.LoadingStatusGrid.ActualHeight + 16), currentMainWindow.LoadingStatusGrid.Translation.Z), new Vector3(0, 0, currentMainWindow.LoadingStatusGrid.Translation.Z)), + currentMainWindow.LoadingStatusGrid.GetElementCompositor()!.CreateScalarKeyFrameAnimation("Opacity", 0, 1)) + ); + + currentMainWindow.LoadingStatusGrid.Visibility = Visibility.Collapsed; currentMainWindow.LoadingStatusBackgroundGrid!.Visibility = Visibility.Collapsed; HideActionButton(); } diff --git a/CollapseLauncher/Classes/Helper/StreamUtility.cs b/CollapseLauncher/Classes/Helper/StreamUtility.cs index cae294b80..7b932743a 100644 --- a/CollapseLauncher/Classes/Helper/StreamUtility.cs +++ b/CollapseLauncher/Classes/Helper/StreamUtility.cs @@ -50,12 +50,14 @@ private static void EnsureFilePathExist([NotNull] string? path) */ internal static FileInfo EnsureNoReadOnly(this FileInfo fileInfo) + => fileInfo.EnsureNoReadOnly(out _); + + internal static FileInfo EnsureNoReadOnly(this FileInfo fileInfo, out bool isFileExist) { - if (!fileInfo.Exists) + if (!(isFileExist = fileInfo.Exists)) return fileInfo; fileInfo.IsReadOnly = false; - fileInfo.Refresh(); return fileInfo; } diff --git a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs index 35493cd82..a32aba646 100644 --- a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs +++ b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs @@ -101,8 +101,8 @@ internal partial class InstallManagerBase public virtual async ValueTask CleanUpGameFiles(bool withDialog = true) { // Get the unused file info asynchronously - List unusedFileInfo = await GetUnusedFileInfoList(withDialog); - + (List, long) unusedFileInfo = await GetUnusedFileInfoList(withDialog); + // Spawn dialog if used if (withDialog) { @@ -113,11 +113,13 @@ public virtual async ValueTask CleanUpGameFiles(bool withDialog = true) mainWindow.overlayFrame.Navigate(typeof(FileCleanupPage), null, new DrillInNavigationTransitionInfo()); } - + if (FileCleanupPage.Current == null) return; - - FileCleanupPage.Current.InjectFileInfoSource(unusedFileInfo); + await FileCleanupPage.Current.InjectFileInfoSource(unusedFileInfo.Item1, unusedFileInfo.Item2); + + LoadingMessageHelper.HideLoadingFrame(); + FileCleanupPage.Current.MenuExitButton.Click += ExitFromOverlay; FileCleanupPage.Current.MenuReScanButton.Click += ExitFromOverlay; FileCleanupPage.Current.MenuReScanButton.Click += async (_, _) => @@ -130,7 +132,7 @@ public virtual async ValueTask CleanUpGameFiles(bool withDialog = true) } // Delete the file straight forward if dialog is not used - foreach (LocalFileInfo fileInfo in unusedFileInfo) + foreach (LocalFileInfo fileInfo in unusedFileInfo.Item1) { TryDeleteReadOnlyFile(fileInfo.FullPath); } @@ -147,7 +149,7 @@ static void ExitFromOverlay(object? sender, RoutedEventArgs args) } } - protected virtual async Task> GetUnusedFileInfoList(bool includeZipCheck) + protected virtual async Task<(List, long)> GetUnusedFileInfoList(bool includeZipCheck) { LoadingMessageHelper.ShowLoadingFrame(); try @@ -170,9 +172,9 @@ protected virtual async Task> GetUnusedFileInfoList(bool inc { // Initialize new proxy-aware HttpClient using HttpClient httpClient = new HttpClientBuilder() - .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) - .SetAllowedDecompression(DecompressionMethods.None) - .Create(); + .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) + .SetAllowedDecompression(DecompressionMethods.None) + .Create(); // Initialize and get game state, then get the latest package info LoadingMessageHelper.SetMessage( @@ -238,9 +240,10 @@ await DownloadOtherAudioPkgVersion(_gameAudioLangListPathStatic, true); } } - + // Add pre-download zips into the ignored list - RegionResourceVersion? packagePreDownloadList = _gameVersionManager.GetGamePreloadZip()?.FirstOrDefault(); + RegionResourceVersion? packagePreDownloadList = + _gameVersionManager.GetGamePreloadZip()?.FirstOrDefault(); if (packagePreDownloadList != null) { var preDownloadZips = new List(); @@ -253,7 +256,7 @@ await DownloadOtherAudioPkgVersion(_gameAudioLangListPathStatic, preDownloadZips.AddRange(packagePreDownloadList.voice_packs .Select(audioRes => new GameInstallPackage(audioRes, - _gamePath) + _gamePath) { PackageType = GameInstallPackageType.Audio @@ -268,17 +271,18 @@ await DownloadOtherAudioPkgVersion(_gameAudioLangListPathStatic, ignoredFiles = ignoredFiles.Concat(preDownloadZips).ToArray(); } } - + if (ignoredFiles.Length > 0) LogWriteLine($"[GetUnusedFileInfoList] Final ignored file list:\r\n{string.Join(", ", ignoredFiles)}", LogType.Scheme, true); - + // Get the list of the local file paths List localFileInfo = []; await GetRelativeLocalFilePaths(localFileInfo, includeZipCheck, gameStateEnum, _token.Token); // Get and filter the unused file from the pkg_versions and ignoredFiles List unusedFileInfo = []; + long unusedFileSize = 0; await Task.Run(() => Parallel.ForEach(localFileInfo, new ParallelOptions { CancellationToken = _token.Token }, @@ -290,14 +294,19 @@ await Task.Run(() => ignoredFiles.ToList())) return; - lock (unusedFileInfo) unusedFileInfo.Add(asset); + lock (unusedFileInfo) + { + Interlocked.Add(ref unusedFileSize, asset.FileSize); + unusedFileInfo.Add(asset); + } })); - return unusedFileInfo; + return (unusedFileInfo, unusedFileSize); } - finally + catch (Exception ex) { - LoadingMessageHelper.HideLoadingFrame(); + ErrorSender.SendException(ex); + return (new List(), 0); } } @@ -465,14 +474,15 @@ protected virtual async Task GetRelativeLocalFilePaths(List local { await Task.Run(() => { - int count = 0; - long totalSize = 0; - string gamePath = _gamePath; - DirectoryInfo dirInfo = new DirectoryInfo(gamePath); + int count = 0; + long totalSize = 0; + string gamePath = _gamePath; + DirectoryInfo dirInfo = new DirectoryInfo(gamePath); + int updateInterval = 100; // Update UI every 100 files + int processedCount = 0; // Do the do in parallel since it will be a really CPU expensive task due to janky checks here and there. - Parallel.ForEach(dirInfo - .EnumerateFiles("*", SearchOption.AllDirectories), + Parallel.ForEach(dirInfo.EnumerateFiles("*", SearchOption.AllDirectories), new ParallelOptions { CancellationToken = token }, (fileInfo, _) => { @@ -481,23 +491,26 @@ await Task.Run(() => // Do the check within the lambda function to possibly check the file // condition in multithread - if (!IsCategorizedAsGameFile(fileInfo, gamePath, includeZipCheck, - gameState, - out LocalFileInfo localFileInfo)) + if (!IsCategorizedAsGameFile(fileInfo, gamePath, includeZipCheck, gameState, out LocalFileInfo localFileInfo)) return; - Interlocked.Add(ref totalSize, - fileInfo.Exists ? fileInfo.Length : 0); + Interlocked.Add(ref totalSize, fileInfo.Exists ? fileInfo.Length : 0); Interlocked.Increment(ref count); - _parentUI.DispatcherQueue.TryEnqueue(() => - LoadingMessageHelper.SetMessage( - Locale.Lang._FileCleanupPage.LoadingTitle, - string - .Format(Locale.Lang._FileCleanupPage.LoadingSubtitle1, - count, - ConverterTool - .SummarizeSizeSimple(totalSize)) - )); + int currentCount = Interlocked.Increment(ref processedCount); + + if (currentCount % updateInterval == 0) + { + _parentUI.DispatcherQueue.TryEnqueue(() => + { + LoadingMessageHelper.SetMessage( + Locale.Lang._FileCleanupPage.LoadingTitle, + string.Format(Locale.Lang._FileCleanupPage.LoadingSubtitle1, + count, + ConverterTool.SummarizeSizeSimple(totalSize)) + ); + }); + } + lock (localFileInfoList) { localFileInfoList.Add(localFileInfo); diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/FileCleanupPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/FileCleanupPage.xaml index 4a2fe1c32..42502bc10 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/FileCleanupPage.xaml +++ b/CollapseLauncher/XAMLs/MainApp/Pages/FileCleanupPage.xaml @@ -156,7 +156,7 @@