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 @@