diff --git a/src/nxDumpFuse.sln.DotSettings b/src/nxDumpFuse.sln.DotSettings new file mode 100644 index 0000000..c3ce823 --- /dev/null +++ b/src/nxDumpFuse.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/nxDumpFuse/App.axaml b/src/nxDumpFuse/App.axaml index bbfa9b7..4cf59f7 100644 --- a/src/nxDumpFuse/App.axaml +++ b/src/nxDumpFuse/App.axaml @@ -3,12 +3,12 @@ xmlns:local="using:nxDumpFuse" x:Class="nxDumpFuse.App"> - + - - - - - - + + + + + + \ No newline at end of file diff --git a/src/nxDumpFuse/DependencyInjection/DialogServiceBootstrapper.cs b/src/nxDumpFuse/DependencyInjection/DialogServiceBootstrapper.cs index 3bce121..71f7e2c 100644 --- a/src/nxDumpFuse/DependencyInjection/DialogServiceBootstrapper.cs +++ b/src/nxDumpFuse/DependencyInjection/DialogServiceBootstrapper.cs @@ -1,6 +1,5 @@ using nxDumpFuse.Extensions; using nxDumpFuse.Interfaces; -using nxDumpFuse.Model; using nxDumpFuse.Services; using Splat; diff --git a/src/nxDumpFuse/Interfaces/IDialogService.cs b/src/nxDumpFuse/Interfaces/IDialogService.cs index e4f5d51..44c6834 100644 --- a/src/nxDumpFuse/Interfaces/IDialogService.cs +++ b/src/nxDumpFuse/Interfaces/IDialogService.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Avalonia.Controls; -using nxDumpFuse.ViewModels; namespace nxDumpFuse.Interfaces { diff --git a/src/nxDumpFuse/Model/Enums/FuseSimpleLogType.cs b/src/nxDumpFuse/Model/Enums/FuseSimpleLogType.cs new file mode 100644 index 0000000..72fc22b --- /dev/null +++ b/src/nxDumpFuse/Model/Enums/FuseSimpleLogType.cs @@ -0,0 +1,8 @@ +namespace nxDumpFuse.Model.Enums +{ + public enum FuseSimpleLogType + { + Error, + Information + } +} diff --git a/src/nxDumpFuse/Model/Fuse.cs b/src/nxDumpFuse/Model/Fuse.cs index 0b687d4..d6db3da 100644 --- a/src/nxDumpFuse/Model/Fuse.cs +++ b/src/nxDumpFuse/Model/Fuse.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using nxDumpFuse.Events; +using nxDumpFuse.Model.Enums; namespace nxDumpFuse.Model { @@ -26,7 +26,6 @@ namespace nxDumpFuse.Model } public event EventHandlers.FuseUpdateEventHandler? FuseUpdateEvent; - public event EventHandlers.FuseSimpleLogEventHandler? FuseSimpleLogEvent; protected virtual void OnFuseUpdate(FuseUpdateInfo fuseUpdateInfo) @@ -34,6 +33,17 @@ namespace nxDumpFuse.Model FuseUpdateEvent?.Invoke(fuseUpdateInfo); } + private void Update(int part, int parts, double progress, double progressPart) + { + OnFuseUpdate(new FuseUpdateInfo + { + Part = part, + Parts = parts, + Progress = progress, + ProgressPart = progressPart + }); + } + protected virtual void OnFuseSimpleLogEvent(FuseSimpleLog log) { FuseSimpleLogEvent?.Invoke(log); @@ -51,92 +61,98 @@ namespace nxDumpFuse.Model Log(FuseSimpleLogType.Error, "Input File cannot be empty"); return; } - if (string.IsNullOrEmpty(_outputDir)) { Log(FuseSimpleLogType.Error, "Output Directory cannot be empty"); return; } - Log(FuseSimpleLogType.Information, "Fuse Started"); - GetOutputFilePath(_inputFilePath, _outputDir); + + GetOutputFilePath(); if (string.IsNullOrEmpty(_outputFilePath)) { Log(FuseSimpleLogType.Error, "Output path was null"); return; } + var inputFiles = GetInputFiles(); if (inputFiles.Length == 0) { Log(FuseSimpleLogType.Error, "No input files found"); return; } + FuseFiles(inputFiles, _outputFilePath); } - private void GetOutputFilePath(string inputFilePath, string outputDir) + private void GetOutputFilePath() { - string? fileName = Path.GetFileName(inputFilePath); + var fileName = Path.GetFileName(_inputFilePath); if (Path.HasExtension(fileName)) { - string ext = Path.GetExtension(fileName).Replace(".", string.Empty); - List split = fileName.Split(".").ToList(); + 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}"); + _outputFilePath = Path.Join(_outputDir, $"{string.Join("", split.Take(split.Count - 2))}.{XciExt}"); } 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}"); - else switch (ext.Substring(0, 2)) + _outputFilePath = Path.Join(_outputDir, $"{string.Join("", split.Take(split.Count - 2))}.{NspExt}"); + else switch (ext[..2]) { // .xc0 case "xc" when int.TryParse(ext.Substring(ext.Length - 1, 1), out _): - _outputFilePath = Path.Join(outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{XciExt}"); + _outputFilePath = Path.Join(_outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{XciExt}"); break; // .ns0 case "ns" when int.TryParse(ext.Substring(ext.Length - 1, 1), out _): - _outputFilePath = Path.Join(outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{NspExt}"); + _outputFilePath = Path.Join(_outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{NspExt}"); break; } } else // dir/00 { - var inputDir = new FileInfo(inputFilePath).Directory?.Name; + 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 + _outputFilePath = Path.Join(_outputDir, inputDirSplit.Length == 1 ? $"{inputDir}.{NspExt}" : $"{string.Join("", (inputDirSplit).Take(inputDirSplit.Length - 1))}.{NspExt}"); } } - private async void FuseFiles(string[] inputFiles, string outputFilePath) + private async void FuseFiles(IReadOnlyCollection inputFiles, string outputFilePath) { var buffer = new byte[1024 * 1024]; var count = 0; long totalBytes = 0; var totalFileLength = GetTotalFileSize(inputFiles); - Log(FuseSimpleLogType.Information, $"Fusing {inputFiles.Length} parts to {outputFilePath}:{totalFileLength / 1000}kB"); + + Log(FuseSimpleLogType.Information, $"Fusing {inputFiles.Count} parts to {outputFilePath} ({ToMb(totalFileLength)}MB)"); + await using var outputStream = File.Create(outputFilePath); foreach (var inputFilePath in inputFiles) { if (_cts.Token.IsCancellationRequested) return; - ++count; - await using var inputStream = File.OpenRead(inputFilePath); - Log(FuseSimpleLogType.Information, $"Fusing file part {count} --> {inputFilePath}/{inputStream.Length / 1000}kB"); - var fileLength = inputStream.Length; long currentBytes = 0; int currentBlockSize; + await using var inputStream = File.OpenRead(inputFilePath); + var fileLength = inputStream.Length; + + Log(FuseSimpleLogType.Information, $"Fusing file part {++count}-> {inputFilePath} ({ToMb(fileLength)}MB)"); + while ((currentBlockSize = inputStream.Read(buffer, 0, buffer.Length)) > 0) { if (_cts.Token.IsCancellationRequested) return; + currentBytes += currentBlockSize; totalBytes += currentBlockSize; @@ -149,20 +165,21 @@ namespace nxDumpFuse.Model Log(FuseSimpleLogType.Error, e.Message); } - OnFuseUpdate(new FuseUpdateInfo - { - Part = count, - Parts = inputFiles.Length, - ProgressPart = currentBytes * 100.0 / fileLength, - Progress = totalBytes * 100.0 / totalFileLength - }); + var progress = totalBytes * 100.0 / totalFileLength; + var progressPart = currentBytes * 100.0 / fileLength; + Update(count, inputFiles.Count, progress, progressPart); } } Log(FuseSimpleLogType.Information, "Fuse Complete"); } - private static long GetTotalFileSize(string[] inputFiles) + private static long ToMb(long bytes) + { + return bytes / 1000000; + } + + private static long GetTotalFileSize(IEnumerable inputFiles) { long totalFileSize = 0; inputFiles.Select(f => f).ToList().ForEach(f => totalFileSize += new FileInfo(f).Length); @@ -179,14 +196,9 @@ namespace nxDumpFuse.Model public void StopFuse() { _cts.Cancel(); + Log(FuseSimpleLogType.Information, "Fuse Stopped"); - OnFuseUpdate(new FuseUpdateInfo - { - Part = 0, - Parts = 0, - ProgressPart = 0, - Progress = 0 - }); + if (File.Exists(_outputFilePath)) { Task.Run((() => @@ -198,6 +210,7 @@ namespace nxDumpFuse.Model { File.Delete(_outputFilePath); Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => Log(FuseSimpleLogType.Information, $"Deleted {_outputFilePath}")); + Update(0, 0, 0, 0); break; } catch (IOException) diff --git a/src/nxDumpFuse/Model/FuseSimpleLog.cs b/src/nxDumpFuse/Model/FuseSimpleLog.cs index 4b39644..8b4b576 100644 --- a/src/nxDumpFuse/Model/FuseSimpleLog.cs +++ b/src/nxDumpFuse/Model/FuseSimpleLog.cs @@ -1,15 +1,9 @@ using System; -using System.Diagnostics; using Avalonia.Media; +using nxDumpFuse.Model.Enums; namespace nxDumpFuse.Model { - public enum FuseSimpleLogType - { - Error, - Information - } - public class FuseSimpleLog { public FuseSimpleLog(FuseSimpleLogType type, DateTime time, string message) @@ -17,8 +11,9 @@ namespace nxDumpFuse.Model Type = type; Time = time; Message = message; + // This is not working, seems to be an issue https://github.com/AvaloniaUI/Avalonia/issues/2482 + // needs to be reviewed. Setting Foreground property directly works fine, however binding fails Color = Type == FuseSimpleLogType.Information ? Brushes.White : Brushes.Red; - Debug.WriteLine($"Color is {Color}"); } public FuseSimpleLogType Type { get; } diff --git a/src/nxDumpFuse/Services/DialogService.cs b/src/nxDumpFuse/Services/DialogService.cs index d9a41c6..7cfc904 100644 --- a/src/nxDumpFuse/Services/DialogService.cs +++ b/src/nxDumpFuse/Services/DialogService.cs @@ -1,18 +1,18 @@ using System.Threading.Tasks; -using Avalonia; using Avalonia.Controls; using nxDumpFuse.Interfaces; -using nxDumpFuse.ViewModels; namespace nxDumpFuse.Services { public class DialogService : IDialogService { private readonly IMainWindowProvider _mainWindowProvider; + public DialogService(IMainWindowProvider mainWindowProvider) { _mainWindowProvider = mainWindowProvider; } + public async Task ShowOpenFileDialogAsync(string title, FileDialogFilter filter) { var openFileDialog = new OpenFileDialog() diff --git a/src/nxDumpFuse/ViewModels/AboutViewModel.cs b/src/nxDumpFuse/ViewModels/AboutViewModel.cs index 8e6645a..a970c62 100644 --- a/src/nxDumpFuse/ViewModels/AboutViewModel.cs +++ b/src/nxDumpFuse/ViewModels/AboutViewModel.cs @@ -9,8 +9,7 @@ namespace nxDumpFuse.ViewModels { public class AboutViewModel : ViewModelBase, IAboutViewModel { - private string _gitHubUrl = "https://github.com/oMaN-Rod/nxDumpFuse"; - private string _explorer = "explorer.exe"; + private const string GitHubUrl = "https://github.com/oMaN-Rod/nxDumpFuse"; public AboutViewModel() { @@ -20,28 +19,29 @@ namespace nxDumpFuse.ViewModels public ReactiveCommand OpenGithubCommand { get; } public string UsageText => BuildUsageText(); + public string AuthorInfo => "Made for fun by oMaN-Rod, check me out on -->"; - private void OpenGithub() + private static void OpenGithub() { try { - Process.Start(_explorer, _gitHubUrl); + Process.Start("explorer.exe", GitHubUrl); } catch { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - Process.Start("xdg-open", _gitHubUrl); + Process.Start("xdg-open", GitHubUrl); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - Process.Start("open", _gitHubUrl); + Process.Start("open", GitHubUrl); } } } - private string BuildUsageText() + private static string BuildUsageText() { var sb = new StringBuilder(); sb.AppendLine("To fuse files make sure all file parts are alone in a directory with no other files, i.e"); diff --git a/src/nxDumpFuse/ViewModels/FuseViewModel.cs b/src/nxDumpFuse/ViewModels/FuseViewModel.cs index 3b1769c..3250f73 100644 --- a/src/nxDumpFuse/ViewModels/FuseViewModel.cs +++ b/src/nxDumpFuse/ViewModels/FuseViewModel.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Reactive; using Avalonia.Controls; using nxDumpFuse.Interfaces; using nxDumpFuse.Model; -using nxDumpFuse.Services; +using nxDumpFuse.Model.Enums; using ReactiveUI; namespace nxDumpFuse.ViewModels diff --git a/src/nxDumpFuse/Views/AboutView.axaml b/src/nxDumpFuse/Views/AboutView.axaml index 072f93b..f801152 100644 --- a/src/nxDumpFuse/Views/AboutView.axaml +++ b/src/nxDumpFuse/Views/AboutView.axaml @@ -2,26 +2,20 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:imaging="clr-namespace:Avalonia.Media.Imaging;assembly=Avalonia.Visuals" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="nxDumpFuse.Views.AboutView" xmlns:vm="clr-namespace:nxDumpFuse.ViewModels" x:DataType="vm:AboutViewModel"> - - - - - - - - - - - - - - + + + + + + \ No newline at end of file diff --git a/src/nxDumpFuse/Views/FuseView.axaml b/src/nxDumpFuse/Views/FuseView.axaml index 4b1623f..3a94afa 100644 --- a/src/nxDumpFuse/Views/FuseView.axaml +++ b/src/nxDumpFuse/Views/FuseView.axaml @@ -5,58 +5,53 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="nxDumpFuse.Views.FuseView" xmlns:vm="clr-namespace:nxDumpFuse.ViewModels" - x:DataType="vm:FuseViewModel" - Name="FuseViewControl"> - - - - - - - - - - - - - - + x:DataType="vm:FuseViewModel"> + + -