|
|
|
@@ -7,9 +7,7 @@ using ICSharpCode.SharpZipLib.Zip;
|
|
|
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
|
using Ryujinx.Ava;
|
|
|
|
|
using Ryujinx.Ava.Common.Locale;
|
|
|
|
|
using Ryujinx.Ava.UI.Controls;
|
|
|
|
|
using Ryujinx.Ava.UI.Helpers;
|
|
|
|
|
using Ryujinx.Ava.UI.Windows;
|
|
|
|
|
using Ryujinx.Common;
|
|
|
|
|
using Ryujinx.Common.Logging;
|
|
|
|
|
using Ryujinx.Ui.Common.Helper;
|
|
|
|
@@ -32,7 +30,6 @@ namespace Ryujinx.Modules
|
|
|
|
|
internal static class Updater
|
|
|
|
|
{
|
|
|
|
|
private const string GitHubApiURL = "https://api.github.com";
|
|
|
|
|
internal static bool Running;
|
|
|
|
|
|
|
|
|
|
private static readonly string HomeDir = AppDomain.CurrentDomain.BaseDirectory;
|
|
|
|
|
private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
|
|
|
|
@@ -43,20 +40,19 @@ namespace Ryujinx.Modules
|
|
|
|
|
private static string _platformExt;
|
|
|
|
|
private static string _buildUrl;
|
|
|
|
|
private static long _buildSize;
|
|
|
|
|
private static bool _updateSuccessful;
|
|
|
|
|
private static bool _running;
|
|
|
|
|
|
|
|
|
|
private static readonly string[] WindowsDependencyDirs = Array.Empty<string>();
|
|
|
|
|
|
|
|
|
|
public static bool UpdateSuccessful { get; private set; }
|
|
|
|
|
|
|
|
|
|
public static async Task BeginParse(MainWindow mainWindow, bool showVersionUpToDate)
|
|
|
|
|
public static async Task BeginParse(Window mainWindow, bool showVersionUpToDate)
|
|
|
|
|
{
|
|
|
|
|
if (Running)
|
|
|
|
|
if (_running)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Running = true;
|
|
|
|
|
mainWindow.ViewModel.CanUpdate = false;
|
|
|
|
|
_running = true;
|
|
|
|
|
|
|
|
|
|
// Detect current platform
|
|
|
|
|
if (OperatingSystem.IsMacOS())
|
|
|
|
@@ -82,21 +78,25 @@ namespace Ryujinx.Modules
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
|
|
|
|
|
|
|
|
|
|
Dispatcher.UIThread.Post(async () =>
|
|
|
|
|
{
|
|
|
|
|
await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage], LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
|
|
|
|
await ContentDialogHelper.CreateWarningDialog(
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_running = false;
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get latest version number from GitHub API
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using (HttpClient jsonClient = ConstructHttpClient())
|
|
|
|
|
{
|
|
|
|
|
string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
|
|
|
|
|
using HttpClient jsonClient = ConstructHttpClient();
|
|
|
|
|
|
|
|
|
|
string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
|
|
|
|
|
string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL);
|
|
|
|
|
JObject jsonRoot = JObject.Parse(fetchedJson);
|
|
|
|
|
JToken assets = jsonRoot["assets"];
|
|
|
|
@@ -123,6 +123,8 @@ namespace Ryujinx.Modules
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_running = false;
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -141,18 +143,22 @@ namespace Ryujinx.Modules
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_running = false;
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception exception)
|
|
|
|
|
{
|
|
|
|
|
Logger.Error?.Print(LogClass.Application, exception.Message);
|
|
|
|
|
|
|
|
|
|
Dispatcher.UIThread.Post(async () =>
|
|
|
|
|
{
|
|
|
|
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_running = false;
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -163,11 +169,16 @@ namespace Ryujinx.Modules
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!");
|
|
|
|
|
|
|
|
|
|
Dispatcher.UIThread.Post(async () =>
|
|
|
|
|
{
|
|
|
|
|
await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage], LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
|
|
|
|
await ContentDialogHelper.CreateWarningDialog(
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_running = false;
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -181,8 +192,7 @@ namespace Ryujinx.Modules
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Running = false;
|
|
|
|
|
mainWindow.ViewModel.CanUpdate = true;
|
|
|
|
|
_running = false;
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -210,7 +220,8 @@ namespace Ryujinx.Modules
|
|
|
|
|
Dispatcher.UIThread.Post(async () =>
|
|
|
|
|
{
|
|
|
|
|
// Show a message asking the user if they want to update
|
|
|
|
|
var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
|
|
|
|
var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.RyujinxUpdaterMessage],
|
|
|
|
|
$"{Program.Version} -> {newVersion}");
|
|
|
|
|
|
|
|
|
@@ -218,12 +229,16 @@ namespace Ryujinx.Modules
|
|
|
|
|
{
|
|
|
|
|
UpdateRyujinx(mainWindow, _buildUrl);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_running = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static HttpClient ConstructHttpClient()
|
|
|
|
|
{
|
|
|
|
|
HttpClient result = new HttpClient();
|
|
|
|
|
HttpClient result = new();
|
|
|
|
|
|
|
|
|
|
// Required by GitHub to interract with APIs.
|
|
|
|
|
result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
|
|
|
|
@@ -233,7 +248,7 @@ namespace Ryujinx.Modules
|
|
|
|
|
|
|
|
|
|
public static async void UpdateRyujinx(Window parent, string downloadUrl)
|
|
|
|
|
{
|
|
|
|
|
UpdateSuccessful = false;
|
|
|
|
|
_updateSuccessful = false;
|
|
|
|
|
|
|
|
|
|
// Empty update dir, although it shouldn't ever have anything inside it
|
|
|
|
|
if (Directory.Exists(UpdateDir))
|
|
|
|
@@ -245,17 +260,16 @@ namespace Ryujinx.Modules
|
|
|
|
|
|
|
|
|
|
string updateFile = Path.Combine(UpdateDir, "update.bin");
|
|
|
|
|
|
|
|
|
|
var taskDialog = new TaskDialog()
|
|
|
|
|
TaskDialog taskDialog = new()
|
|
|
|
|
{
|
|
|
|
|
Header = LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
|
|
|
|
SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterDownloading],
|
|
|
|
|
IconSource = new SymbolIconSource { Symbol = Symbol.Download },
|
|
|
|
|
Buttons = { },
|
|
|
|
|
ShowProgressBar = true
|
|
|
|
|
ShowProgressBar = true,
|
|
|
|
|
XamlRoot = parent
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
taskDialog.XamlRoot = parent;
|
|
|
|
|
|
|
|
|
|
taskDialog.Opened += (s, e) =>
|
|
|
|
|
{
|
|
|
|
|
if (_buildSize >= 0)
|
|
|
|
@@ -270,7 +284,7 @@ namespace Ryujinx.Modules
|
|
|
|
|
|
|
|
|
|
await taskDialog.ShowAsync(true);
|
|
|
|
|
|
|
|
|
|
if (UpdateSuccessful)
|
|
|
|
|
if (_updateSuccessful)
|
|
|
|
|
{
|
|
|
|
|
var shouldRestart = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterCompleteMessage],
|
|
|
|
@@ -305,8 +319,8 @@ namespace Ryujinx.Modules
|
|
|
|
|
int totalProgressPercentage = 0;
|
|
|
|
|
int[] progressPercentage = new int[ConnectionCount];
|
|
|
|
|
|
|
|
|
|
List<byte[]> list = new List<byte[]>(ConnectionCount);
|
|
|
|
|
List<WebClient> webClients = new List<WebClient>(ConnectionCount);
|
|
|
|
|
List<byte[]> list = new(ConnectionCount);
|
|
|
|
|
List<WebClient> webClients = new(ConnectionCount);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < ConnectionCount; i++)
|
|
|
|
|
{
|
|
|
|
@@ -317,9 +331,9 @@ namespace Ryujinx.Modules
|
|
|
|
|
{
|
|
|
|
|
#pragma warning disable SYSLIB0014
|
|
|
|
|
// TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
|
|
|
|
|
using (WebClient client = new WebClient())
|
|
|
|
|
using WebClient client = new();
|
|
|
|
|
#pragma warning restore SYSLIB0014
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
webClients.Add(client);
|
|
|
|
|
|
|
|
|
|
if (i == ConnectionCount - 1)
|
|
|
|
@@ -405,20 +419,18 @@ namespace Ryujinx.Modules
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void DoUpdateWithSingleThreadWorker(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
|
|
|
|
{
|
|
|
|
|
using (HttpClient client = new HttpClient())
|
|
|
|
|
{
|
|
|
|
|
using HttpClient client = new();
|
|
|
|
|
// We do not want to timeout while downloading
|
|
|
|
|
client.Timeout = TimeSpan.FromDays(1);
|
|
|
|
|
|
|
|
|
|
using (HttpResponseMessage response = client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead).Result)
|
|
|
|
|
using (Stream remoteFileStream = response.Content.ReadAsStreamAsync().Result)
|
|
|
|
|
{
|
|
|
|
|
using (Stream updateFileStream = File.Open(updateFile, FileMode.Create))
|
|
|
|
|
{
|
|
|
|
|
using Stream updateFileStream = File.Open(updateFile, FileMode.Create);
|
|
|
|
|
|
|
|
|
|
long totalBytes = response.Content.Headers.ContentLength.Value;
|
|
|
|
|
long byteWritten = 0;
|
|
|
|
|
|
|
|
|
@@ -440,11 +452,9 @@ namespace Ryujinx.Modules
|
|
|
|
|
updateFileStream.Write(buffer, 0, readSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InstallUpdate(taskDialog, updateFile);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
private static double GetPercentage(double value, double max)
|
|
|
|
@@ -454,8 +464,11 @@ namespace Ryujinx.Modules
|
|
|
|
|
|
|
|
|
|
private static void DoUpdateWithSingleThread(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
|
|
|
|
{
|
|
|
|
|
Thread worker = new Thread(() => DoUpdateWithSingleThreadWorker(taskDialog, downloadUrl, updateFile));
|
|
|
|
|
worker.Name = "Updater.SingleThreadWorker";
|
|
|
|
|
Thread worker = new(() => DoUpdateWithSingleThreadWorker(taskDialog, downloadUrl, updateFile))
|
|
|
|
|
{
|
|
|
|
|
Name = "Updater.SingleThreadWorker"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
worker.Start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -483,10 +496,10 @@ namespace Ryujinx.Modules
|
|
|
|
|
|
|
|
|
|
if (OperatingSystem.IsLinux())
|
|
|
|
|
{
|
|
|
|
|
using (Stream inStream = File.OpenRead(updateFile))
|
|
|
|
|
using (Stream gzipStream = new GZipInputStream(inStream))
|
|
|
|
|
using (TarInputStream tarStream = new TarInputStream(gzipStream, Encoding.ASCII))
|
|
|
|
|
{
|
|
|
|
|
using Stream inStream = File.OpenRead(updateFile);
|
|
|
|
|
using GZipInputStream gzipStream = new(inStream);
|
|
|
|
|
using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
|
|
|
|
|
|
|
|
|
|
await Task.Run(() =>
|
|
|
|
|
{
|
|
|
|
|
TarEntry tarEntry;
|
|
|
|
@@ -516,12 +529,11 @@ namespace Ryujinx.Modules
|
|
|
|
|
|
|
|
|
|
taskDialog.SetProgressBarState(100, TaskDialogProgressState.Normal);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
using (Stream inStream = File.OpenRead(updateFile))
|
|
|
|
|
using (ZipFile zipFile = new ZipFile(inStream))
|
|
|
|
|
{
|
|
|
|
|
using Stream inStream = File.OpenRead(updateFile);
|
|
|
|
|
using ZipFile zipFile = new(inStream);
|
|
|
|
|
|
|
|
|
|
await Task.Run(() =>
|
|
|
|
|
{
|
|
|
|
|
double count = 0;
|
|
|
|
@@ -549,7 +561,6 @@ namespace Ryujinx.Modules
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete downloaded zip
|
|
|
|
|
File.Delete(updateFile);
|
|
|
|
@@ -594,21 +605,24 @@ namespace Ryujinx.Modules
|
|
|
|
|
|
|
|
|
|
SetFileExecutable(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx"));
|
|
|
|
|
|
|
|
|
|
UpdateSuccessful = true;
|
|
|
|
|
_updateSuccessful = true;
|
|
|
|
|
|
|
|
|
|
taskDialog.Hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
|
|
|
|
public static bool CanUpdate(bool showWarnings, StyleableWindow parent)
|
|
|
|
|
public static bool CanUpdate(bool showWarnings)
|
|
|
|
|
{
|
|
|
|
|
#if !DISABLE_UPDATER
|
|
|
|
|
if (RuntimeInformation.OSArchitecture != Architecture.X64)
|
|
|
|
|
{
|
|
|
|
|
if (showWarnings)
|
|
|
|
|
{
|
|
|
|
|
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedMessage],
|
|
|
|
|
Dispatcher.UIThread.Post(async () =>
|
|
|
|
|
{
|
|
|
|
|
await ContentDialogHelper.CreateWarningDialog(
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedMessage],
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedSubMessage]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
@@ -618,8 +632,12 @@ namespace Ryujinx.Modules
|
|
|
|
|
{
|
|
|
|
|
if (showWarnings)
|
|
|
|
|
{
|
|
|
|
|
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage],
|
|
|
|
|
Dispatcher.UIThread.Post(async () =>
|
|
|
|
|
{
|
|
|
|
|
await ContentDialogHelper.CreateWarningDialog(
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage],
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
@@ -629,8 +647,12 @@ namespace Ryujinx.Modules
|
|
|
|
|
{
|
|
|
|
|
if (showWarnings)
|
|
|
|
|
{
|
|
|
|
|
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage],
|
|
|
|
|
Dispatcher.UIThread.Post(async () =>
|
|
|
|
|
{
|
|
|
|
|
await ContentDialogHelper.CreateWarningDialog(
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage],
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
@@ -642,18 +664,27 @@ namespace Ryujinx.Modules
|
|
|
|
|
{
|
|
|
|
|
if (ReleaseInformation.IsFlatHubBuild())
|
|
|
|
|
{
|
|
|
|
|
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage]);
|
|
|
|
|
Dispatcher.UIThread.Post(async () =>
|
|
|
|
|
{
|
|
|
|
|
await ContentDialogHelper.CreateWarningDialog(
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]);
|
|
|
|
|
Dispatcher.UIThread.Post(async () =>
|
|
|
|
|
{
|
|
|
|
|
await ContentDialogHelper.CreateWarningDialog(
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
|
|
|
|
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
|
|
|
|
|
|
|
|
|
// NOTE: This method should always reflect the latest build layout.s
|
|
|
|
|
private static IEnumerable<string> EnumerateFilesToDelete()
|
|
|
|
@@ -677,7 +708,7 @@ namespace Ryujinx.Modules
|
|
|
|
|
|
|
|
|
|
private static void MoveAllFilesOver(string root, string dest, TaskDialog taskDialog)
|
|
|
|
|
{
|
|
|
|
|
var total = Directory.GetFiles(root, "*", SearchOption.AllDirectories).Length;
|
|
|
|
|
int total = Directory.GetFiles(root, "*", SearchOption.AllDirectories).Length;
|
|
|
|
|
foreach (string directory in Directory.GetDirectories(root))
|
|
|
|
|
{
|
|
|
|
|
string dirName = Path.GetFileName(directory);
|
|
|
|
@@ -694,6 +725,7 @@ namespace Ryujinx.Modules
|
|
|
|
|
foreach (string file in Directory.GetFiles(root))
|
|
|
|
|
{
|
|
|
|
|
count++;
|
|
|
|
|
|
|
|
|
|
File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true);
|
|
|
|
|
|
|
|
|
|
Dispatcher.UIThread.InvokeAsync(() =>
|
|
|
|
|