From 6764c93b5912b3d087ca7c957db00e3f8550fff0 Mon Sep 17 00:00:00 2001
From: Bagus Nur Listiyono <28079733+bagusnl@users.noreply.github.com>
Date: Wed, 17 Jan 2024 21:52:16 +0700
Subject: [PATCH 01/51] [skip ci] Update version info
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 0d995efa5..14eb84e49 100644
--- a/README.md
+++ b/README.md
@@ -210,7 +210,7 @@ Not only that, this launcher also has some advanced features for **Genshin Impac
> **Note**: The version for this build is `1.72.14` (Released on: December 30th, 2023).
[](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.72.14-pre/CL-1.72.14-preview_Installer.exe)
-> **Note**: The version for this build is `1.73.0` (Released on: January 10th, 2024).
+> **Note**: The version for this build is `1.73.1` (Released on: January 17th, 2024).
To view all releases, [**click here**](https://github.com/neon-nyan/CollapseLauncher/releases).
@@ -277,4 +277,4 @@ Made by all captains around the world with ❤️. Fight for all that is beautif
[SignPath Foundation]:https://signpath.org
-[SignPath.io]:https://signpath.io
\ No newline at end of file
+[SignPath.io]:https://signpath.io
From d5ad7514d528b93278ef7d75285cef0cd639f136 Mon Sep 17 00:00:00 2001
From: Kemal Setya Adhi
Date: Wed, 17 Jan 2024 22:43:25 +0700
Subject: [PATCH 02/51] Bring Back "Move to Different Location" feature while
migrating game (#370)
* Initial Implementation
* Fix crash if conversion selection is empty while cancelled
* Adjust ``ContentDialogCollapse`` style
+ Set the minimum height from 164 to 64 pixel
+ Set the button column width to be automatic
* Use proper permission check using Windows Security Principal based
* Update local changes
+ Fix TextBlock size becoming too small
+ Move every segment of the code into separate parts
+ Fix MoveWriteFileInner() to write output data with incorrect size
+ Reduce refresh rate for the events to 100ms each
* Add "Move Game Location" feature to Quick Settings
* Fix button state when game is not installed or having update
* Lock only CurrentFileCountMoved while updating status
* Wrap text instead of overflowing
* Add path check if the input and output are the same
* Do not invoke ``MainWindow``'s ``ContentDialog`` to show dialog
---
.../Classes/FileMigrationProcess/Events.cs | 67 +++++
.../FileMigrationProcess.cs | 160 ++++++++++++
.../FileMigrationProcessRef.cs | 16 ++
.../Classes/FileMigrationProcess/IO.cs | 140 +++++++++++
.../Classes/FileMigrationProcess/Statics.cs | 55 ++++
.../Classes/FileMigrationProcess/UIBuilder.cs | 235 ++++++++++++++++++
.../BaseClass/InstallManagerBase.cs | 72 +++++-
.../Classes/Interfaces/IGameInstallManager.cs | 2 +-
.../Classes/UIElementExtensions.cs | 120 +++++++++
.../Pages/Dialogs/InstallationConvert.xaml.cs | 3 +
.../MainApp/Pages/Dialogs/SimpleDialogs.cs | 46 ++++
.../XAMLs/MainApp/Pages/HomePage.xaml | 8 +
.../XAMLs/MainApp/Pages/HomePage.xaml.cs | 23 ++
.../XAMLs/Theme/ContentDialogCollapse.xaml | 12 +-
Hi3Helper.Core/Lang/Locale/LangDialogs.cs | 12 +
.../Lang/Locale/LangFileMigrationProcess.cs | 24 ++
Hi3Helper.Core/Lang/Locale/LangHomePage.cs | 1 +
Hi3Helper.Core/Lang/Locale/LangMisc.cs | 6 +
18 files changed, 993 insertions(+), 9 deletions(-)
create mode 100644 CollapseLauncher/Classes/FileMigrationProcess/Events.cs
create mode 100644 CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs
create mode 100644 CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcessRef.cs
create mode 100644 CollapseLauncher/Classes/FileMigrationProcess/IO.cs
create mode 100644 CollapseLauncher/Classes/FileMigrationProcess/Statics.cs
create mode 100644 CollapseLauncher/Classes/FileMigrationProcess/UIBuilder.cs
create mode 100644 CollapseLauncher/Classes/UIElementExtensions.cs
create mode 100644 Hi3Helper.Core/Lang/Locale/LangFileMigrationProcess.cs
diff --git a/CollapseLauncher/Classes/FileMigrationProcess/Events.cs b/CollapseLauncher/Classes/FileMigrationProcess/Events.cs
new file mode 100644
index 000000000..6c0802e75
--- /dev/null
+++ b/CollapseLauncher/Classes/FileMigrationProcess/Events.cs
@@ -0,0 +1,67 @@
+using Hi3Helper;
+using Hi3Helper.Data;
+using System;
+using System.Threading.Tasks;
+
+namespace CollapseLauncher
+{
+ internal partial class FileMigrationProcess
+ {
+ private void UpdateCountProcessed(FileMigrationProcessUIRef uiRef, string currentPathProcessed)
+ {
+ lock (this) { this.CurrentFileCountMoved++; }
+
+ string fileCountProcessedString = string.Format(Locale.Lang._Misc.PerFromTo,
+ this.CurrentFileCountMoved,
+ this.TotalFileCount);
+
+ lock (uiRef.fileCountIndicatorSubtitle)
+ {
+ this.parentUI.DispatcherQueue.TryEnqueue(() =>
+ {
+ uiRef.fileCountIndicatorSubtitle.Text = fileCountProcessedString;
+ uiRef.pathActivitySubtitle.Text = currentPathProcessed;
+ });
+ }
+ }
+
+ private async void UpdateSizeProcessed(FileMigrationProcessUIRef uiRef, long currentRead)
+ {
+ lock (this) { this.CurrentSizeMoved += currentRead; }
+
+ if (await CheckIfNeedRefreshStopwatch())
+ {
+ double percentage = Math.Round((double)this.CurrentSizeMoved / this.TotalFileSize * 100d, 2);
+ double speed = this.CurrentSizeMoved / this.ProcessStopwatch.Elapsed.TotalSeconds;
+
+ lock (uiRef.progressBarIndicator)
+ {
+ this.parentUI.DispatcherQueue.TryEnqueue(() =>
+ {
+ string speedString = string.Format(Locale.Lang._Misc.SpeedPerSec, ConverterTool.SummarizeSizeSimple(speed));
+ string sizeProgressString = string.Format(Locale.Lang._Misc.PerFromTo,
+ ConverterTool.SummarizeSizeSimple(this.CurrentSizeMoved),
+ ConverterTool.SummarizeSizeSimple(this.TotalFileSize));
+
+ uiRef.speedIndicatorSubtitle.Text = speedString;
+ uiRef.fileSizeIndicatorSubtitle.Text = sizeProgressString;
+ uiRef.progressBarIndicator.Value = percentage;
+ uiRef.progressBarIndicator.IsIndeterminate = false;
+ });
+ }
+ }
+ }
+
+ private async ValueTask CheckIfNeedRefreshStopwatch()
+ {
+ if (this.EventsStopwatch.ElapsedMilliseconds > _refreshInterval)
+ {
+ this.EventsStopwatch.Restart();
+ return true;
+ }
+
+ await Task.Delay(_refreshInterval);
+ return false;
+ }
+ }
+}
diff --git a/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs b/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs
new file mode 100644
index 000000000..a431c299b
--- /dev/null
+++ b/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs
@@ -0,0 +1,160 @@
+using Microsoft.UI.Xaml;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace CollapseLauncher
+{
+ internal partial class FileMigrationProcess
+ {
+ private const int _refreshInterval = 100; // 100ms UI refresh interval
+
+ private string dialogTitle { get; set; }
+ private string inputPath { get; set; }
+ private string outputPath { get; set; }
+ private bool isFileTransfer { get; set; }
+ private UIElement parentUI { get; set; }
+ private CancellationTokenSource tokenSource { get; set; }
+
+ private long CurrentSizeMoved;
+ private long CurrentFileCountMoved;
+ private long TotalFileSize;
+ private long TotalFileCount;
+ private bool IsSameOutputDrive;
+ private Stopwatch ProcessStopwatch;
+ private Stopwatch EventsStopwatch;
+
+ private FileMigrationProcess(UIElement parentUI, string dialogTitle, string inputPath, string outputPath, bool isFileTransfer, CancellationTokenSource tokenSource)
+ {
+ this.dialogTitle = dialogTitle;
+ this.inputPath = inputPath;
+ this.outputPath = outputPath;
+ this.isFileTransfer = isFileTransfer;
+ this.parentUI = parentUI;
+ this.tokenSource = tokenSource;
+ }
+
+ internal async Task StartRoutine()
+ {
+ bool isSuccess = false;
+ this.CurrentSizeMoved = 0;
+ this.CurrentFileCountMoved = 0;
+ this.TotalFileSize = 0;
+ this.TotalFileCount = 0;
+ FileMigrationProcessUIRef? uiRef = null;
+
+ try
+ {
+ if (!await IsOutputPathSpaceSufficient(this.inputPath, this.outputPath))
+ throw new OperationCanceledException($"Disk space is not sufficient. Cancelling!");
+
+ uiRef = BuildMainMigrationUI();
+ string outputPath = await StartRoutineInner(uiRef.Value);
+ uiRef.Value.mainDialogWindow.Hide();
+ isSuccess = true;
+
+ return outputPath;
+ }
+ catch when (!isSuccess) // Throw if the isSuccess is not set to true
+ {
+ if (uiRef.HasValue && uiRef.Value.mainDialogWindow != null)
+ {
+ uiRef.Value.mainDialogWindow.Hide();
+ await Task.Delay(500); // Give artificial delay to give main dialog window thread to close first
+ }
+ throw;
+ }
+ finally
+ {
+ if (ProcessStopwatch != null) ProcessStopwatch.Stop();
+ if (EventsStopwatch != null) EventsStopwatch.Stop();
+ }
+ }
+
+ private async Task StartRoutineInner(FileMigrationProcessUIRef uiRef)
+ {
+ this.ProcessStopwatch = Stopwatch.StartNew();
+ this.EventsStopwatch = Stopwatch.StartNew();
+ return this.isFileTransfer ? await MoveFile(uiRef) : await MoveDirectory(uiRef);
+ }
+
+ private async Task MoveFile(FileMigrationProcessUIRef uiRef)
+ {
+ FileInfo inputPathInfo = new FileInfo(this.inputPath);
+ FileInfo outputPathInfo = new FileInfo(this.outputPath);
+
+ string inputPathDir = Path.GetDirectoryName(inputPathInfo.FullName);
+ string outputPathDir = Path.GetDirectoryName(outputPathInfo.FullName);
+
+ if (!Directory.Exists(outputPathDir))
+ Directory.CreateDirectory(outputPathDir);
+
+ // Update path display
+ string inputFileBasePath = inputPathInfo.FullName.Substring(inputPathDir.Length + 1);
+ UpdateCountProcessed(uiRef, inputFileBasePath);
+
+ if (this.IsSameOutputDrive)
+ {
+ // Logger.LogWriteLine($"[FileMigrationProcess::MoveFile()] Moving file in the same drive from: {inputPathInfo.FullName} to {outputPathInfo.FullName}", LogType.Default, true);
+ inputPathInfo.MoveTo(outputPathInfo.FullName);
+ UpdateSizeProcessed(uiRef, inputPathInfo.Length);
+ }
+ else
+ {
+ // Logger.LogWriteLine($"[FileMigrationProcess::MoveFile()] Moving file across different drives from: {inputPathInfo.FullName} to {outputPathInfo.FullName}", LogType.Default, true);
+ await MoveWriteFile(uiRef, inputPathInfo, outputPathInfo, this.tokenSource == null ? default : this.tokenSource.Token);
+ }
+
+ return outputPathInfo.FullName;
+ }
+
+ private async Task MoveDirectory(FileMigrationProcessUIRef uiRef)
+ {
+ DirectoryInfo inputPathInfo = new DirectoryInfo(this.inputPath);
+ if (!Directory.Exists(this.outputPath))
+ Directory.CreateDirectory(this.outputPath);
+
+ DirectoryInfo outputPathInfo = new DirectoryInfo(this.outputPath);
+
+ int parentInputPathLength = inputPathInfo.Parent.FullName.Length + 1;
+ string outputDirBaseNamePath = inputPathInfo.FullName.Substring(parentInputPathLength);
+ string outputDirPath = Path.Combine(this.outputPath, outputDirBaseNamePath);
+
+ await Parallel.ForEachAsync(
+ inputPathInfo.EnumerateFiles("*", SearchOption.AllDirectories),
+ this.tokenSource?.Token ?? default,
+ async (inputFileInfo, cancellationToken) =>
+ {
+ int parentInputPathLength = inputPathInfo.Parent.FullName.Length + 1;
+ string inputFileBasePath = inputFileInfo.FullName.Substring(parentInputPathLength);
+
+ // Update path display
+ UpdateCountProcessed(uiRef, inputFileBasePath);
+
+ string outputTargetPath = Path.Combine(outputPathInfo.FullName, inputFileBasePath);
+ string outputTargetDirPath = Path.GetDirectoryName(outputTargetPath);
+
+ if (!Directory.Exists(outputTargetDirPath))
+ Directory.CreateDirectory(outputTargetDirPath);
+
+ if (this.IsSameOutputDrive)
+ {
+ // Logger.LogWriteLine($"[FileMigrationProcess::MoveDirectory()] Moving directory content in the same drive from: {inputFileInfo.FullName} to {outputTargetPath}", LogType.Default, true);
+ inputFileInfo.MoveTo(outputTargetPath);
+ UpdateSizeProcessed(uiRef, inputFileInfo.Length);
+ }
+ else
+ {
+ // Logger.LogWriteLine($"[FileMigrationProcess::MoveDirectory()] Moving directory content across different drives from: {inputFileInfo.FullName} to {outputTargetPath}", LogType.Default, true);
+ FileInfo outputFileInfo = new FileInfo(outputTargetPath);
+ await MoveWriteFile(uiRef, inputFileInfo, outputFileInfo, cancellationToken);
+ }
+ });
+
+ inputPathInfo.Delete(true);
+ return outputDirPath;
+ }
+ }
+}
diff --git a/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcessRef.cs b/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcessRef.cs
new file mode 100644
index 000000000..9e8b7cb02
--- /dev/null
+++ b/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcessRef.cs
@@ -0,0 +1,16 @@
+using CollapseLauncher.CustomControls;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Documents;
+
+namespace CollapseLauncher
+{
+ internal struct FileMigrationProcessUIRef
+ {
+ internal ContentDialogCollapse mainDialogWindow;
+ internal TextBlock pathActivitySubtitle;
+ internal Run speedIndicatorSubtitle;
+ internal Run fileCountIndicatorSubtitle;
+ internal Run fileSizeIndicatorSubtitle;
+ internal ProgressBar progressBarIndicator;
+ }
+}
diff --git a/CollapseLauncher/Classes/FileMigrationProcess/IO.cs b/CollapseLauncher/Classes/FileMigrationProcess/IO.cs
new file mode 100644
index 000000000..38e8fe750
--- /dev/null
+++ b/CollapseLauncher/Classes/FileMigrationProcess/IO.cs
@@ -0,0 +1,140 @@
+using CollapseLauncher.CustomControls;
+using CollapseLauncher.Dialogs;
+using Hi3Helper;
+using Hi3Helper.Data;
+using Microsoft.UI.Xaml.Controls;
+using System;
+using System.Buffers;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace CollapseLauncher
+{
+ internal partial class FileMigrationProcess
+ {
+ private async Task MoveWriteFile(FileMigrationProcessUIRef uiRef, FileInfo inputFile, FileInfo outputFile, CancellationToken token)
+ {
+ int bufferSize = 1 << 18; // 256 kB Buffer
+
+ if (inputFile.Length < bufferSize)
+ bufferSize = (int)inputFile.Length;
+
+ bool isUseArrayPool = Environment.ProcessorCount * bufferSize > 2 << 20;
+ byte[] buffer = isUseArrayPool ? ArrayPool.Shared.Rent(bufferSize) : new byte[bufferSize];
+
+ try
+ {
+ await using (FileStream inputStream = inputFile.OpenRead())
+ await using (FileStream outputStream = outputFile.Exists && outputFile.Length <= inputFile.Length ? outputFile.Open(FileMode.Open) : outputFile.Create())
+ {
+ // Just in-case if the previous move is incomplete, then update and seek to the last position.
+ if (outputFile.Length <= inputStream.Length && outputFile.Length >= bufferSize)
+ {
+ // Do check by comparing the first and last 128K data of the file
+ Memory firstCompareInputBytes = new byte[bufferSize];
+ Memory firstCompareOutputBytes = new byte[bufferSize];
+ Memory lastCompareInputBytes = new byte[bufferSize];
+ Memory lastCompareOutputBytes = new byte[bufferSize];
+
+ // Seek to the first data
+ inputStream.Position = 0;
+ await inputStream.ReadExactlyAsync(firstCompareInputBytes);
+ outputStream.Position = 0;
+ await outputStream.ReadExactlyAsync(firstCompareOutputBytes);
+
+ // Seek to the last data
+ long lastPos = outputStream.Length - bufferSize;
+ inputStream.Position = lastPos;
+ await inputStream.ReadExactlyAsync(lastCompareInputBytes);
+ outputStream.Position = lastPos;
+ await outputStream.ReadExactlyAsync(lastCompareOutputBytes);
+
+ bool isMatch = firstCompareInputBytes.Span.SequenceEqual(firstCompareOutputBytes.Span)
+ && lastCompareInputBytes.Span.SequenceEqual(lastCompareOutputBytes.Span);
+
+ // If the buffers don't match, then start the copy from the beginning
+ if (!isMatch)
+ {
+ inputStream.Position = 0;
+ outputStream.Position = 0;
+ }
+ else
+ {
+ UpdateSizeProcessed(uiRef, outputStream.Length);
+ }
+ }
+
+ await MoveWriteFileInner(uiRef, inputStream, outputStream, buffer, token);
+ }
+
+ inputFile.IsReadOnly = false;
+ inputFile.Delete();
+ }
+ catch { throw; } // Re-throw to
+ finally
+ {
+ if (isUseArrayPool) ArrayPool.Shared.Return(buffer);
+ }
+ }
+
+ private async Task MoveWriteFileInner(FileMigrationProcessUIRef uiRef, FileStream inputStream, FileStream outputStream, byte[] buffer, CancellationToken token)
+ {
+ int read;
+ while ((read = await inputStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
+ {
+ await outputStream.WriteAsync(buffer, 0, read, token);
+ UpdateSizeProcessed(uiRef, read);
+ }
+ }
+
+ private async ValueTask IsOutputPathSpaceSufficient(string inputPath, string outputPath)
+ {
+ DriveInfo inputDriveInfo = new DriveInfo(Path.GetPathRoot(inputPath));
+ DriveInfo outputDriveInfo = new DriveInfo(Path.GetPathRoot(outputPath));
+
+ this.TotalFileSize = await Task.Run(() =>
+ {
+ if (this.isFileTransfer)
+ {
+ FileInfo fileInfo = new FileInfo(inputPath);
+ return fileInfo.Length;
+ }
+
+ DirectoryInfo directoryInfo = new DirectoryInfo(inputPath);
+ long returnSize = directoryInfo.EnumerateFiles("*", SearchOption.AllDirectories).Sum(x =>
+ {
+ this.TotalFileCount++;
+ return x.Length;
+ });
+ return returnSize;
+ });
+
+ if (IsSameOutputDrive = inputDriveInfo.Name == outputDriveInfo.Name)
+ return true;
+
+ bool isSpaceSufficient = outputDriveInfo.TotalFreeSpace < this.TotalFileSize;
+ if (!isSpaceSufficient)
+ {
+ string errStr = $"Free Space on {outputDriveInfo.Name} is not sufficient! (Free space: {outputDriveInfo.TotalFreeSpace}, Req. Space: {this.TotalFileSize}, Drive: {outputDriveInfo.Name})";
+ Logger.LogWriteLine(errStr, LogType.Error, true);
+ await SimpleDialogs.SpawnDialog(
+ string.Format(Locale.Lang._Dialogs.OperationErrorDiskSpaceInsufficientTitle, outputDriveInfo.Name),
+ string.Format(Locale.Lang._Dialogs.OperationErrorDiskSpaceInsufficientMsg,
+ ConverterTool.SummarizeSizeSimple(outputDriveInfo.TotalFreeSpace),
+ ConverterTool.SummarizeSizeSimple(this.TotalFileSize),
+ outputDriveInfo.Name),
+ parentUI,
+ null,
+ Locale.Lang._Misc.Okay,
+ null,
+ ContentDialogButton.Primary,
+ ContentDialogTheme.Error
+ );
+ }
+
+ return isSpaceSufficient;
+ }
+ }
+}
diff --git a/CollapseLauncher/Classes/FileMigrationProcess/Statics.cs b/CollapseLauncher/Classes/FileMigrationProcess/Statics.cs
new file mode 100644
index 000000000..3add3c2bd
--- /dev/null
+++ b/CollapseLauncher/Classes/FileMigrationProcess/Statics.cs
@@ -0,0 +1,55 @@
+using CollapseLauncher.Dialogs;
+using Hi3Helper.Data;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace CollapseLauncher
+{
+ internal partial class FileMigrationProcess
+ {
+ internal static async Task CreateJob(UIElement parentUI, string dialogTitle, string inputPath, string outputPath = null, CancellationTokenSource token = default, bool showWarningMessage = true)
+ {
+ // Normalize Path (also normalize path from '/' separator)
+ inputPath = ConverterTool.NormalizePath(inputPath);
+
+ // Check whether the input is a file or not.
+ bool isFileTransfer = File.Exists(inputPath) && !inputPath.StartsWith('\\');
+ outputPath = await InitializeAndCheckOutputPath(parentUI, dialogTitle, inputPath, outputPath, isFileTransfer);
+ if (outputPath == null) return null;
+
+ if (showWarningMessage)
+ if (await ShowNotCancellableProcedureMessage(parentUI) == ContentDialogResult.None)
+ return null;
+
+ return new FileMigrationProcess(parentUI, dialogTitle, inputPath, outputPath, isFileTransfer, token);
+ }
+
+ private static async ValueTask InitializeAndCheckOutputPath(UIElement parentUI, string dialogTitle, string inputPath, string outputPath, bool isFileTransfer)
+ {
+ if (!string.IsNullOrEmpty(outputPath)
+ && ConverterTool.IsUserHasPermission(outputPath)
+ && !IsOutputPathSameAsInput(inputPath, outputPath, isFileTransfer))
+ return outputPath;
+
+ return await BuildCheckOutputPathUI(parentUI, dialogTitle, inputPath, outputPath, isFileTransfer);
+ }
+
+ private static bool IsOutputPathSameAsInput(string inputPath, string outputPath, bool isFilePath)
+ {
+ bool isStringEmpty = string.IsNullOrEmpty(outputPath);
+
+ if (!isFilePath) inputPath = Path.GetDirectoryName(inputPath);
+ bool isPathEqual = inputPath.AsSpan().TrimEnd('\\').SequenceEqual(outputPath.AsSpan().TrimEnd('\\'));
+
+ if (isStringEmpty) return true;
+ return isPathEqual;
+ }
+
+ private static async Task ShowNotCancellableProcedureMessage(UIElement parentUI) => await SimpleDialogs.Dialog_WarningOperationNotCancellable(parentUI);
+
+ }
+}
diff --git a/CollapseLauncher/Classes/FileMigrationProcess/UIBuilder.cs b/CollapseLauncher/Classes/FileMigrationProcess/UIBuilder.cs
new file mode 100644
index 000000000..2f7d21023
--- /dev/null
+++ b/CollapseLauncher/Classes/FileMigrationProcess/UIBuilder.cs
@@ -0,0 +1,235 @@
+using CollapseLauncher.CustomControls;
+using CollapseLauncher.FileDialogCOM;
+using Hi3Helper;
+using Hi3Helper.Data;
+using Microsoft.UI.Text;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Documents;
+using Microsoft.UI.Xaml.Media;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace CollapseLauncher
+{
+ internal partial class FileMigrationProcess
+ {
+ private static async ValueTask BuildCheckOutputPathUI(UIElement parentUI, string dialogTitle, string inputPath, string outputPath, bool isFileTransfer)
+ {
+ ContentDialogCollapse mainDialogWindow = new ContentDialogCollapse(ContentDialogTheme.Informational)
+ {
+ Title = dialogTitle,
+ CloseButtonText = Locale.Lang._Misc.Cancel,
+ PrimaryButtonText = null,
+ SecondaryButtonText = null,
+ DefaultButton = ContentDialogButton.Primary,
+ XamlRoot = parentUI.XamlRoot
+ };
+
+ Grid mainGrid = new Grid();
+ mainGrid.AddGridRows(3);
+ mainGrid.AddGridColumns(1, new GridLength(1.0, GridUnitType.Star));
+ mainGrid.AddGridColumns(1);
+
+ TextBlock locateFolderSubtitle = mainGrid.AddElementToGridColumn(new TextBlock
+ {
+ FontSize = 16d,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ TextWrapping = TextWrapping.Wrap,
+ Text = Locale.Lang._FileMigrationProcess.LocateFolderSubtitle
+ }, 0, 2);
+
+ TextBox choosePathTextBox = mainGrid.AddElementToGridRow(new TextBox
+ {
+ Margin = new Thickness(0d, 12d, 0d, 0d),
+ IsSpellCheckEnabled = false,
+ IsRightTapEnabled = false,
+ Width = 500,
+ PlaceholderText = Locale.Lang._FileMigrationProcess.ChoosePathTextBoxPlaceholder,
+ Text = string.IsNullOrEmpty(outputPath) ? null : outputPath
+ }, 1);
+
+ Button choosePathButton = mainGrid
+ .AddElementToGridRowColumn(UIElementExtensions
+ .CreateButtonWithIcon