1
0
Fork 0
mirror of https://github.com/oMaN-Rod/nxDumpFuse.git synced 2024-11-26 04:02:18 +00:00

Add speed, progress, and clean up Fuse

This commit is contained in:
Omar 2021-10-29 13:35:20 -04:00
parent dbfff7fabe
commit a13cf7c091
8 changed files with 251 additions and 175 deletions

View file

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace nxDumpFuse.Extensions
{
public static class ListExtensions
{
public static long GetOutputFileSize(this List<string> files)
{
long totalFileSize = 0;
files.Select(f => f).ToList().ForEach(f => totalFileSize += new FileInfo(f).Length);
return totalFileSize;
}
}
}

View file

@ -0,0 +1,16 @@
namespace nxDumpFuse.Extensions
{
public static class LongExtensions
{
public static long ToMb(this long bytes)
{
return bytes / (1024 * 1024);
}
public static long ToSeconds(this long milliseconds)
{
return milliseconds / 1000;
}
}
}

View file

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using nxDumpFuse.Model.Enums;
namespace nxDumpFuse.Extensions
{
public static class StringExtensions
{
public static List<string> GetInputFiles(this string inputFilePath, FileCase fileCase)
{
var inputDir = Path.GetDirectoryName(inputFilePath);
if (string.IsNullOrEmpty(inputDir)) return new List<string>();
var files = new List<string>();
switch (fileCase)
{
case FileCase.XciNumeric: // .xci.00
case FileCase.NspNumeric: // .nsp.00
files = Directory.GetFiles(inputDir)
.Where(f => int.TryParse(Path.GetExtension(f).Replace(".", ""), out _))
.ToList();
break;
case FileCase.Xci: // .xc0
case FileCase.Nsp: // .ns0
files = Directory.GetFiles(inputDir, $"{Path.GetFileNameWithoutExtension(inputFilePath)}*")
.ToList();
break;
case FileCase.Numeric: // dir/00
files = Directory.GetFiles(inputDir)
.Where(f => int.TryParse(Path.GetFileName(f), out _))
.ToList();
break;
}
files.Sort();
return files;
}
public static Tuple<string,FileCase> GetOutputFilePath(this string inputFilePath, string outputDir)
{
var fileName = Path.GetFileName(inputFilePath);
string outputFilePath;
const string xciExt = "xci";
const string nspExt = "nsp";
if (Path.HasExtension(fileName))
{
var ext = Path.GetExtension(fileName).Replace(".", string.Empty);
var split = fileName.Split(".").ToList();
if (int.TryParse(ext, out _) && split.Count >= 3 && split[^2] == xciExt) // .xci.00
{
outputFilePath = Path.Join(outputDir, $"{string.Join("", split.Take(split.Count - 2))}.{xciExt}");
return new Tuple<string, FileCase>(outputFilePath, FileCase.XciNumeric);
}
if (int.TryParse(ext, out _) && split.Count >= 3 && split[^2] == nspExt) // .nsp.00
{
outputFilePath = Path.Join(outputDir, $"{string.Join("", split.Take(split.Count - 2))}.{nspExt}");
return new Tuple<string, FileCase>(outputFilePath, FileCase.NspNumeric);
}
switch (ext[..2])
{
// .xc0
case "xc" when int.TryParse(ext.Substring(ext.Length - 1, 1), out _):
outputFilePath = Path.Join(outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{xciExt}");
return new Tuple<string, FileCase>(outputFilePath, FileCase.Xci);
// .ns0
case "ns" when int.TryParse(ext.Substring(ext.Length - 1, 1), out _):
outputFilePath = Path.Join(outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{nspExt}");
return new Tuple<string, FileCase>(outputFilePath, FileCase.Nsp);
}
}
else // dir/00
{
var inputDir = new FileInfo(inputFilePath).Directory?.Name;
if (string.IsNullOrEmpty(inputDir))
{
inputDir = Path.GetPathRoot(inputFilePath);
outputFilePath = $"{inputDir}.{nspExt}";
return new Tuple<string, FileCase>(outputFilePath, FileCase.Numeric);
}
var inputDirSplit = inputDir.Split(".");
outputFilePath = Path.Join(outputDir, inputDirSplit.Length == 1
? $"{inputDir}.{nspExt}"
: $"{string.Join("", (inputDirSplit).Take(inputDirSplit.Length - 1))}.{nspExt}");
return new Tuple<string, FileCase>(outputFilePath, FileCase.Numeric);
}
return new Tuple<string, FileCase>(string.Empty, FileCase.Invalid);
}
}
}

View file

@ -2,6 +2,7 @@
{ {
public enum FileCase public enum FileCase
{ {
Invalid,
XciNumeric, // .xci.00 XciNumeric, // .xci.00
NspNumeric, // .nsp.00 NspNumeric, // .nsp.00
Xci, // .xc0 Xci, // .xc0

View file

@ -2,23 +2,23 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using nxDumpFuse.Events; using nxDumpFuse.Events;
using nxDumpFuse.Extensions;
using nxDumpFuse.Model.Enums; using nxDumpFuse.Model.Enums;
namespace nxDumpFuse.Model namespace nxDumpFuse.Model
{ {
public class Fuse public class Fuse
{ {
private const string XciExt = "xci";
private const string NspExt = "nsp";
private readonly CancellationTokenSource _cts; private readonly CancellationTokenSource _cts;
private readonly string _inputFilePath; private readonly string _inputFilePath;
private readonly string _outputDir; private readonly string _outputDir;
private string _outputFilePath = string.Empty; private string _outputFilePath = string.Empty;
private FileCase _fileCase; private FileCase _fileCase;
private readonly Stopwatch _sw = new();
public Fuse(string inputFilePath, string outputDir) public Fuse(string inputFilePath, string outputDir)
{ {
@ -35,14 +35,16 @@ namespace nxDumpFuse.Model
FuseUpdateEvent?.Invoke(fuseUpdateInfo); FuseUpdateEvent?.Invoke(fuseUpdateInfo);
} }
private void Update(int part, int parts, double progress, double progressPart) private void Update(int part, int parts, double progress, double progressPart, long speed, bool complete = false)
{ {
OnFuseUpdate(new FuseUpdateInfo OnFuseUpdate(new FuseUpdateInfo
{ {
Part = part, Part = part,
Parts = parts, Parts = parts,
Progress = progress, Progress = progress,
ProgressPart = progressPart ProgressPart = progressPart,
Speed = speed.ToMb(),
Complete = complete
}); });
} }
@ -56,7 +58,7 @@ namespace nxDumpFuse.Model
OnFuseSimpleLogEvent(new FuseSimpleLog(type, DateTime.Now, message)); OnFuseSimpleLogEvent(new FuseSimpleLog(type, DateTime.Now, message));
} }
public void FuseDump() public void Start()
{ {
if (string.IsNullOrEmpty(_inputFilePath)) if (string.IsNullOrEmpty(_inputFilePath))
{ {
@ -69,14 +71,14 @@ namespace nxDumpFuse.Model
return; return;
} }
GetOutputFilePath(); (_outputFilePath, _fileCase) = _inputFilePath.GetOutputFilePath(_outputDir);
if (string.IsNullOrEmpty(_outputFilePath)) if (string.IsNullOrEmpty(_outputFilePath) || _fileCase == FileCase.Invalid)
{ {
Log(FuseSimpleLogType.Error, "Output path was null"); Log(FuseSimpleLogType.Error, "Output path was null");
return; return;
} }
var inputFiles = GetInputFiles(); var inputFiles = _inputFilePath.GetInputFiles(_fileCase);
if (inputFiles.Count == 0) if (inputFiles.Count == 0)
{ {
Log(FuseSimpleLogType.Error, "No input files found"); Log(FuseSimpleLogType.Error, "No input files found");
@ -86,77 +88,58 @@ namespace nxDumpFuse.Model
FuseFiles(inputFiles); FuseFiles(inputFiles);
} }
private void GetOutputFilePath() public void Stop()
{ {
_cts.Cancel();
_sw.Stop();
var fileName = Path.GetFileName(_inputFilePath); Log(FuseSimpleLogType.Information, "Fuse Stopped");
if (Path.HasExtension(fileName))
{
var ext = Path.GetExtension(fileName).Replace(".", string.Empty);
var split = fileName.Split(".").ToList();
if (int.TryParse(ext, out _) && split.Count >= 3 && split[^2] == XciExt) // .xci.00 if (File.Exists(_outputFilePath))
{ {
_outputFilePath = Path.Join(_outputDir, $"{string.Join("", split.Take(split.Count - 2))}.{XciExt}"); Task.Run((() =>
_fileCase = FileCase.XciNumeric;
}
else if (int.TryParse(ext, out _) && split.Count >= 3 && split[^2] == NspExt) // .nsp.00
{ {
_outputFilePath = Path.Join(_outputDir, $"{string.Join("", split.Take(split.Count - 2))}.{NspExt}"); const int retries = 5;
_fileCase = FileCase.NspNumeric; for (var i = 0; i <= retries; i++)
}
else switch (ext[..2])
{ {
// .xc0 try
case "xc" when int.TryParse(ext.Substring(ext.Length - 1, 1), out _): {
_outputFilePath = Path.Join(_outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{XciExt}"); File.Delete(_outputFilePath);
_fileCase = FileCase.Xci; Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => Log(FuseSimpleLogType.Information, $"Deleted {_outputFilePath}"));
break; Update(0, 0, 0, 0, 0);
// .ns0
case "ns" when int.TryParse(ext.Substring(ext.Length - 1, 1), out _):
_outputFilePath = Path.Join(_outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{NspExt}");
_fileCase = FileCase.Nsp;
break; break;
} }
} catch (IOException)
else // dir/00
{ {
_fileCase = FileCase.Numeric; Thread.Sleep(1000);
var inputDir = new FileInfo(_inputFilePath).Directory?.Name;
if (string.IsNullOrEmpty(inputDir))
{
inputDir = Path.GetPathRoot(_inputFilePath);
_outputFilePath = $"{inputDir}.{NspExt}";
return;
} }
}
var inputDirSplit = inputDir.Split("."); }));
_outputFilePath = Path.Join(_outputDir, inputDirSplit.Length == 1
? $"{inputDir}.{NspExt}"
: $"{string.Join("", (inputDirSplit).Take(inputDirSplit.Length - 1))}.{NspExt}");
} }
} }
private async void FuseFiles(IReadOnlyCollection<string> inputFiles) private async void FuseFiles(List<string> inputFiles)
{ {
var buffer = new byte[1024 * 1024]; var buffer = new byte[1024 * 1024];
var count = 0; var count = 0;
long totalBytes = 0; long totalBytes = 0;
var totalFileLength = GetTotalFileSize(inputFiles); var totalFileLength = inputFiles.GetOutputFileSize();
Log(FuseSimpleLogType.Information, $"Fusing {inputFiles.Count} parts to {_outputFilePath} ({ToMb(totalFileLength)}MB)"); Log(FuseSimpleLogType.Information, $"Fusing {inputFiles.Count} parts to {_outputFilePath} ({totalFileLength.ToMb()}MB)");
_sw.Start();
await using var outputStream = File.Create(_outputFilePath); await using var outputStream = File.Create(_outputFilePath);
foreach (var inputFilePath in inputFiles) foreach (var inputFilePath in inputFiles)
{ {
if (_cts.Token.IsCancellationRequested) return; if (_cts.Token.IsCancellationRequested) return;
long currentBytes = 0; long currentBytes = 0;
int currentBlockSize; int currentBlockSize;
long copySpeed = 0;
await using var inputStream = File.OpenRead(inputFilePath); await using var inputStream = File.OpenRead(inputFilePath);
var fileLength = inputStream.Length; var fileLength = inputStream.Length;
Log(FuseSimpleLogType.Information, $"Fusing file part {++count}-> {inputFilePath} ({ToMb(fileLength)}MB)"); Log(FuseSimpleLogType.Information, $"Fusing file part {++count}-> {inputFilePath} ({fileLength.ToMb()}MB)");
while ((currentBlockSize = inputStream.Read(buffer, 0, buffer.Length)) > 0) while ((currentBlockSize = inputStream.Read(buffer, 0, buffer.Length)) > 0)
{ {
@ -172,84 +155,21 @@ namespace nxDumpFuse.Model
catch (TaskCanceledException e) catch (TaskCanceledException e)
{ {
Log(FuseSimpleLogType.Error, e.Message); Log(FuseSimpleLogType.Error, e.Message);
_sw.Stop();
Update(0,0,0,0,0,true);
return;
} }
var progress = totalBytes * 100.0 / totalFileLength; var progress = totalBytes * 100.0 / totalFileLength;
var progressPart = currentBytes * 100.0 / fileLength; var progressPart = currentBytes * 100.0 / fileLength;
Update(count, inputFiles.Count, progress, progressPart); if(_sw.ElapsedMilliseconds >= 1000) copySpeed = totalBytes / _sw.ElapsedMilliseconds.ToSeconds();
Update(count, inputFiles.Count, progress, progressPart, copySpeed);
} }
} }
Log(FuseSimpleLogType.Information, "Fuse Complete"); Log(FuseSimpleLogType.Information, $"Fuse Completed in {_sw.ElapsedMilliseconds.ToSeconds()}s");
} _sw.Stop();
Update(0, 0, 0, 0, 0, true);
private static long ToMb(long bytes)
{
return bytes / 1000000;
}
private static long GetTotalFileSize(IEnumerable<string> inputFiles)
{
long totalFileSize = 0;
inputFiles.Select(f => f).ToList().ForEach(f => totalFileSize += new FileInfo(f).Length);
return totalFileSize;
}
private List<string> GetInputFiles()
{
var inputDir = Path.GetDirectoryName(_inputFilePath);
if (string.IsNullOrEmpty(inputDir)) return new List<string>();
var files = new List<string>();
switch (_fileCase)
{
case FileCase.XciNumeric: // .xci.00
case FileCase.NspNumeric: // .nsp.00
files = Directory.GetFiles(inputDir)
.Where(f => int.TryParse(Path.GetExtension(f).Replace(".", ""), out _))
.ToList();
break;
case FileCase.Xci: // .xc0
case FileCase.Nsp: // .ns0
files = Directory.GetFiles(inputDir, $"{Path.GetFileNameWithoutExtension(_inputFilePath)}*")
.ToList();
break;
case FileCase.Numeric: // dir/00
files = Directory.GetFiles(inputDir)
.Where(f => int.TryParse(Path.GetFileName(f), out _))
.ToList();
break;
}
files.Sort();
return files;
}
public void StopFuse()
{
_cts.Cancel();
Log(FuseSimpleLogType.Information, "Fuse Stopped");
if (File.Exists(_outputFilePath))
{
Task.Run((() =>
{
const int retries = 5;
for (var i = 0; i <= retries; i++)
{
try
{
File.Delete(_outputFilePath);
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => Log(FuseSimpleLogType.Information, $"Deleted {_outputFilePath}"));
Update(0, 0, 0, 0);
break;
}
catch (IOException)
{
Thread.Sleep(1000);
}
}
}));
}
} }
} }
} }

View file

@ -6,8 +6,12 @@
public double ProgressPart { get; set; } public double ProgressPart { get; set; }
public long Speed { get; set; }
public int Part { get; set; } public int Part { get; set; }
public int Parts { get; set; } public int Parts { get; set; }
public bool Complete { get; set; }
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reactive; using System.Reactive;
using Avalonia.Controls; using Avalonia.Controls;
using nxDumpFuse.Interfaces; using nxDumpFuse.Interfaces;
@ -14,6 +15,8 @@ namespace nxDumpFuse.ViewModels
{ {
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private Fuse? _fuse; private Fuse? _fuse;
private readonly Stopwatch _sw = new();
private TimeSpan _elapsed;
public FuseViewModel(IDialogService dialogService) public FuseViewModel(IDialogService dialogService)
{ {
@ -37,7 +40,6 @@ namespace nxDumpFuse.ViewModels
public ReactiveCommand<Unit, Unit> StopCommand { get; } public ReactiveCommand<Unit, Unit> StopCommand { get; }
private string _inputFilePath = string.Empty; private string _inputFilePath = string.Empty;
public string InputFilePath public string InputFilePath
{ {
get => _inputFilePath; get => _inputFilePath;
@ -45,7 +47,6 @@ namespace nxDumpFuse.ViewModels
} }
private string _outputDir = string.Empty; private string _outputDir = string.Empty;
public string OutputDir public string OutputDir
{ {
get => _outputDir; get => _outputDir;
@ -53,7 +54,6 @@ namespace nxDumpFuse.ViewModels
} }
private string _progressPartText = string.Empty; private string _progressPartText = string.Empty;
public string ProgressPartText public string ProgressPartText
{ {
get => _progressPartText; get => _progressPartText;
@ -61,7 +61,6 @@ namespace nxDumpFuse.ViewModels
} }
private double _progressPart; private double _progressPart;
public double ProgressPart public double ProgressPart
{ {
get => _progressPart; get => _progressPart;
@ -69,15 +68,20 @@ namespace nxDumpFuse.ViewModels
} }
private double _progress; private double _progress;
public double Progress public double Progress
{ {
get => _progress; get => _progress;
set => this.RaiseAndSetIfChanged(ref _progress, value); set => this.RaiseAndSetIfChanged(ref _progress, value);
} }
private ObservableCollection<FuseSimpleLog> _logItems = new(); private string _progressText = string.Empty;
public string ProgressText
{
get => _progressText;
set => this.RaiseAndSetIfChanged(ref _progressText, value);
}
private ObservableCollection<FuseSimpleLog> _logItems = new();
public ObservableCollection<FuseSimpleLog> LogItems public ObservableCollection<FuseSimpleLog> LogItems
{ {
get => _logItems; get => _logItems;
@ -99,18 +103,22 @@ namespace nxDumpFuse.ViewModels
_fuse = new Fuse(InputFilePath, OutputDir); _fuse = new Fuse(InputFilePath, OutputDir);
_fuse.FuseUpdateEvent += OnFuseUpdate; _fuse.FuseUpdateEvent += OnFuseUpdate;
_fuse.FuseSimpleLogEvent += OnFuseSimpleLogEvent; _fuse.FuseSimpleLogEvent += OnFuseSimpleLogEvent;
_sw.Start();
try try
{ {
_fuse.FuseDump(); _fuse.Start();
} }
catch (Exception e) { catch (Exception e) {
_sw.Stop();
OnFuseSimpleLogEvent(new FuseSimpleLog(FuseSimpleLogType.Error, DateTime.Now, e.Message)); OnFuseSimpleLogEvent(new FuseSimpleLog(FuseSimpleLogType.Error, DateTime.Now, e.Message));
} }
} }
private void StopDump() private void StopDump()
{ {
_fuse?.StopFuse(); _sw.Stop();
_fuse?.Stop();
ProgressText = string.Empty;
} }
private void ClearLog() private void ClearLog()
@ -120,9 +128,20 @@ namespace nxDumpFuse.ViewModels
private void OnFuseUpdate(FuseUpdateInfo fuseUpdateInfo) private void OnFuseUpdate(FuseUpdateInfo fuseUpdateInfo)
{ {
if (fuseUpdateInfo.Complete)
{
_sw.Stop();
ProgressText = string.Empty;
return;
}
ProgressPart = fuseUpdateInfo.ProgressPart; ProgressPart = fuseUpdateInfo.ProgressPart;
Progress = fuseUpdateInfo.Progress;
ProgressPartText = $"Part {fuseUpdateInfo.Part}/{fuseUpdateInfo.Parts}"; ProgressPartText = $"Part {fuseUpdateInfo.Part}/{fuseUpdateInfo.Parts}";
Progress = fuseUpdateInfo.Progress;
if (!(_sw.Elapsed.TotalSeconds >= 0.5 &&
_sw.Elapsed.TotalSeconds - _elapsed.TotalSeconds >= 0.5)) return;
_elapsed = _sw.Elapsed;
ProgressText = $"({fuseUpdateInfo.Speed:0}MB/s) {Progress:0}% ";
} }
private void OnFuseSimpleLogEvent(FuseSimpleLog log) private void OnFuseSimpleLogEvent(FuseSimpleLog log)

View file

@ -8,40 +8,41 @@
x:DataType="vm:FuseViewModel"> x:DataType="vm:FuseViewModel">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<Grid Margin="20" DockPanel.Dock="Top" <Grid Margin="20" DockPanel.Dock="Top"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto,*"> ColumnDefinitions="Auto,*,50">
<Button Grid.Row="0" Grid.Column="0" Command="{Binding SelectInputFileCommand}" Content="Input" <Button Grid.Row="0" Grid.Column="0" Command="{Binding SelectInputFileCommand}" Content="Input"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" /> HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalAlignment="Center" Text="{Binding InputFilePath}" <TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Margin="5" VerticalAlignment="Center"
Text="{Binding InputFilePath}"
Name="InputFileTextBox" /> Name="InputFileTextBox" />
<Button Grid.Row="1" Grid.Column="0" Command="{Binding SelectOutputFolderCommand}" Content="Output" <Button Grid.Row="1" Grid.Column="0" Command="{Binding SelectOutputFolderCommand}" Content="Output"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" /> HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" />
<TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalAlignment="Center" Text="{Binding OutputDir}" /> <TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="5" VerticalAlignment="Center"
Text="{Binding OutputDir}" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding ProgressPartText}" HorizontalAlignment="Right" <TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding ProgressPartText}" HorizontalAlignment="Right"
Margin="2" /> Margin="2" />
<ProgressBar Grid.Row="2" Grid.Column="1" Margin="2" Height="10" Value="{Binding ProgressPart}" /> <ProgressBar Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="2" Height="10" Value="{Binding ProgressPart}" HorizontalAlignment="Stretch"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Total" HorizontalAlignment="Right" Margin="2" /> <TextBlock Grid.Row="3" Grid.Column="0" Text="Total" HorizontalAlignment="Right" Margin="2" />
<ProgressBar Grid.Row="3" Grid.Column="1" Margin="2" Height="10" Value="{Binding Progress}" /> <ProgressBar Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="2" Height="10" Value="{Binding Progress}" HorizontalAlignment="Stretch"/>
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<StackPanel Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Command="{Binding FuseCommand}" Content="Fuse" HorizontalAlignment="Stretch" <Button Command="{Binding FuseCommand}" Content="Fuse" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" Margin="2" /> HorizontalContentAlignment="Center" Margin="2" />
<Button Command="{Binding StopCommand}" Content="Stop" HorizontalAlignment="Stretch" <Button Command="{Binding StopCommand}" Content="Stop" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" Margin="2" /> HorizontalContentAlignment="Center" Margin="2" />
</StackPanel> </StackPanel>
<Expander Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2" <Expander Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="3"
Header="Log" Header="Log"
Margin="2"> Margin="2">
<Grid> <Grid ColumnDefinitions="*,Auto" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<DataGrid Grid.Column="0" <DataGrid Grid.Column="0"
x:Name="FuseSimpleLog" x:Name="FuseSimpleLog"
Items="{Binding LogItems}" Items="{Binding LogItems}"
@ -60,12 +61,16 @@
<DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="Auto" FontSize="12" /> <DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="Auto" FontSize="12" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<Button Grid.Column="1" Command="{Binding ClearLogCommand}" Content="Clear" HorizontalAlignment="Stretch" <Button Grid.Column="1" Command="{Binding ClearLogCommand}" Content="Clear"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" VerticalAlignment="Top" Margin="4 0 0 0" /> HorizontalContentAlignment="Center" VerticalAlignment="Top" Margin="4 0 0 0" />
</Grid> </Grid>
</Expander> </Expander>
</Grid> </Grid>
<TextBlock DockPanel.Dock="Bottom" Text="{Binding ProgressText}" HorizontalAlignment="Right" VerticalAlignment="Bottom" FontSize="12" Margin="0 0 20 0"/>
</DockPanel> </DockPanel>
</UserControl> </UserControl>