From 6df5e3f7ba39b6a9c86ec1ddeea4ef81149f5903 Mon Sep 17 00:00:00 2001 From: Zaid AlAsali Date: Fri, 5 Jun 2026 17:43:01 +0200 Subject: [PATCH 1/9] feat: add ZaidForge smart output templates --- .../ConversionPreset/ConversionPreset.cs | 9 +- Application/FileConverter/PathHelpers.cs | 101 +++++++++++++++++- .../Properties/Resources.en.resx | 9 +- .../FileConverter/Properties/Resources.resx | 31 +++--- .../ValueConverters/FileNameConverter.cs | 8 +- .../FileConverter/Views/SettingsWindow.xaml | 2 + PresetSamples/ZaidForgeSmartWebpArchive.xml | 33 ++++++ ZAIDFORGE_VNEXT.md | 40 +++++++ 8 files changed, 214 insertions(+), 19 deletions(-) create mode 100644 PresetSamples/ZaidForgeSmartWebpArchive.xml create mode 100644 ZAIDFORGE_VNEXT.md diff --git a/Application/FileConverter/ConversionPreset/ConversionPreset.cs b/Application/FileConverter/ConversionPreset/ConversionPreset.cs index d06d203e..a0eda4fc 100644 --- a/Application/FileConverter/ConversionPreset/ConversionPreset.cs +++ b/Application/FileConverter/ConversionPreset/ConversionPreset.cs @@ -321,7 +321,14 @@ public void RemoveInputType(string inputType) public string GenerateOutputFilePath(string inputFilePath, int numberIndex, int numberMax) { - return PathHelpers.GenerateFilePathFromTemplate(inputFilePath, this.OutputType, this.OutputFileNameTemplate, numberIndex, numberMax); + return PathHelpers.GenerateFilePathFromTemplate( + inputFilePath, + this.OutputType, + this.OutputFileNameTemplate, + numberIndex, + numberMax, + this.ShortName, + this.FullName); } public void SetSettingsValue(string settingsKey, string value) diff --git a/Application/FileConverter/PathHelpers.cs b/Application/FileConverter/PathHelpers.cs index ea0b9aa2..f42acbde 100644 --- a/Application/FileConverter/PathHelpers.cs +++ b/Application/FileConverter/PathHelpers.cs @@ -4,6 +4,7 @@ namespace FileConverter { using System; using System.Collections.Generic; + using System.Globalization; using System.Text; using System.Text.RegularExpressions; @@ -17,6 +18,10 @@ public static class PathHelpers private static Regex filenameRegex = new Regex(@"[^\\]*", RegexOptions.RightToLeft); private static Regex directoryRegex = new Regex(@"^(?\\\\[^\\/:*?""""<>|\r\n]+\\|[A-Za-z]:\\)(?:(?[^\\]*)\\)*"); private static Regex dateRegex = new Regex(@"\(d:(?[^)]*)\)"); + private static Regex sourceCreatedDateRegex = new Regex(@"\((?:sourcecreated|sc):(?[^)]*)\)"); + private static Regex sourceModifiedDateRegex = new Regex(@"\((?:sourcemodified|sm):(?[^)]*)\)"); + private static Regex formattedNumberIndexRegex = new Regex(@"\(n:i:(?[^)]*)\)"); + private static Regex formattedNumberCountRegex = new Regex(@"\(n:c:(?[^)]*)\)"); public static bool IsPathDriveLetterValid(string path) { @@ -139,7 +144,14 @@ public static bool CreateFolders(string filePath) return true; } - public static string GenerateFilePathFromTemplate(string inputFilePath, OutputType outputFileExtension, string outputFilePathTemplate, int numberIndex, int numberMax) + public static string GenerateFilePathFromTemplate( + string inputFilePath, + OutputType outputFileExtension, + string outputFilePathTemplate, + int numberIndex, + int numberMax, + string presetName = null, + string presetFullName = null) { if (string.IsNullOrEmpty(inputFilePath)) { @@ -208,11 +220,96 @@ public static string GenerateFilePathFromTemplate(string inputFilePath, OutputTy outputPath = outputPath.Replace("(n:i)", numberIndex.ToString()); outputPath = outputPath.Replace("(n:c)", numberMax.ToString()); - outputPath = dateRegex.Replace(outputPath, match => DateTime.Now.ToString(match.Groups["format"].Value).Replace('/', '-').Replace(':', '\'')); + outputPath = formattedNumberIndexRegex.Replace(outputPath, match => FormatNumber(numberIndex, match.Groups["format"].Value)); + outputPath = formattedNumberCountRegex.Replace(outputPath, match => FormatNumber(numberMax, match.Groups["format"].Value)); + + string safePresetName = SanitizeFileSystemToken(presetName); + string safePresetPath = SanitizePresetPath(presetFullName ?? presetName); + outputPath = outputPath.Replace("(preset)", safePresetName); + outputPath = outputPath.Replace("(presetname)", safePresetName); + outputPath = outputPath.Replace("(presetpath)", safePresetPath); + + outputPath = dateRegex.Replace(outputPath, match => FormatDate(DateTime.Now, match.Groups["format"].Value)); + outputPath = sourceCreatedDateRegex.Replace(outputPath, match => FormatDate(GetCreationTime(inputFilePath), match.Groups["format"].Value)); + outputPath = sourceModifiedDateRegex.Replace(outputPath, match => FormatDate(GetLastWriteTime(inputFilePath), match.Groups["format"].Value)); outputPath += "." + outputExtension; return outputPath; } + + private static string FormatNumber(int number, string format) + { + if (string.IsNullOrEmpty(format)) + { + return number.ToString(NumberFormatInfo.InvariantInfo); + } + + return number.ToString(format, NumberFormatInfo.InvariantInfo); + } + + private static string FormatDate(DateTime date, string format) + { + if (string.IsNullOrEmpty(format)) + { + return date.ToString(CultureInfo.InvariantCulture).Replace('/', '-').Replace(':', '\''); + } + + return date.ToString(format, CultureInfo.InvariantCulture).Replace('/', '-').Replace(':', '\''); + } + + private static DateTime GetCreationTime(string path) + { + if (!System.IO.File.Exists(path)) + { + return DateTime.Now; + } + + return System.IO.File.GetCreationTime(path); + } + + private static DateTime GetLastWriteTime(string path) + { + if (!System.IO.File.Exists(path)) + { + return DateTime.Now; + } + + return System.IO.File.GetLastWriteTime(path); + } + + private static string SanitizePresetPath(string presetPath) + { + if (string.IsNullOrEmpty(presetPath)) + { + return string.Empty; + } + + string[] segments = presetPath.Split('/'); + for (int index = 0; index < segments.Length; index++) + { + segments[index] = SanitizeFileSystemToken(segments[index]); + } + + return string.Join(System.IO.Path.DirectorySeparatorChar.ToString(), segments); + } + + private static string SanitizeFileSystemToken(string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + char[] invalidFileNameChars = System.IO.Path.GetInvalidFileNameChars(); + StringBuilder builder = new StringBuilder(value.Length); + for (int index = 0; index < value.Length; index++) + { + char character = value[index]; + builder.Append(Array.IndexOf(invalidFileNameChars, character) >= 0 ? '_' : character); + } + + return builder.ToString(); + } } } diff --git a/Application/FileConverter/Properties/Resources.en.resx b/Application/FileConverter/Properties/Resources.en.resx index 7338f453..4957eba5 100644 --- a/Application/FileConverter/Properties/Resources.en.resx +++ b/Application/FileConverter/Properties/Resources.en.resx @@ -268,6 +268,13 @@ ... (n:i): page number index (n:c): total page count +(n:i:D3): formatted page number index +(n:c:D3): formatted page count +(d:yyyy-MM-dd): current date +(sc:yyyy-MM-dd): source file creation date +(sm:yyyy-MM-dd): source file modified date +(preset): selected preset name +(presetpath): selected preset folder path Special paths: (p:d): my documents path @@ -275,7 +282,7 @@ Special paths: (p:v): my videos path (p:p): my pictures path -use maj for uppercase version +Use uppercase tokens for uppercase values when available. Output format diff --git a/Application/FileConverter/Properties/Resources.resx b/Application/FileConverter/Properties/Resources.resx index 11c79680..b5384f17 100644 --- a/Application/FileConverter/Properties/Resources.resx +++ b/Application/FileConverter/Properties/Resources.resx @@ -269,17 +269,24 @@ (d0): input parent folder (d1): input sub parent folder ... -(n:i): page number index -(n:c): total page count - -Special paths: -(p:d): my documents path -(p:m): my music path -(p:v): my videos path -(p:p): my pictures path - -use maj for uppercase version - +(n:i): page number index +(n:c): total page count +(n:i:D3): formatted page number index +(n:c:D3): formatted page count +(d:yyyy-MM-dd): current date +(sc:yyyy-MM-dd): source file creation date +(sm:yyyy-MM-dd): source file modified date +(preset): selected preset name +(presetpath): selected preset folder path + +Special paths: +(p:d): my documents path +(p:m): my music path +(p:v): my videos path +(p:p): my pictures path + +Use uppercase tokens for uppercase values when available. + Output format @@ -616,4 +623,4 @@ use maj for uppercase version Vulkan - \ No newline at end of file + diff --git a/Application/FileConverter/ValueConverters/FileNameConverter.cs b/Application/FileConverter/ValueConverters/FileNameConverter.cs index d8cb0a87..12a458d2 100644 --- a/Application/FileConverter/ValueConverters/FileNameConverter.cs +++ b/Application/FileConverter/ValueConverters/FileNameConverter.cs @@ -10,9 +10,9 @@ public class FileNameConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { - if (values == null || values.Length != 3) + if (values == null || values.Length < 3 || values.Length > 5) { - throw new ArgumentException("The values must contains the input file path, the output file extension and the ouput file template."); + throw new ArgumentException("The values must contain the input file path, the output file extension, the output file template and optional preset names."); } if (!(values[1] is OutputType)) @@ -23,8 +23,10 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur string inputFilePath = values[0] as string; OutputType outputFileExtension = (OutputType)values[1]; string outputFileTemplate = values[2] as string; + string presetName = values.Length > 3 ? values[3] as string : null; + string presetFullName = values.Length > 4 ? values[4] as string : presetName; - return PathHelpers.GenerateFilePathFromTemplate(inputFilePath, outputFileExtension, outputFileTemplate, 1, 3); + return PathHelpers.GenerateFilePathFromTemplate(inputFilePath, outputFileExtension, outputFileTemplate, 1, 3, presetName, presetFullName); } public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture) diff --git a/Application/FileConverter/Views/SettingsWindow.xaml b/Application/FileConverter/Views/SettingsWindow.xaml index a7cd4b87..eef4e1bd 100644 --- a/Application/FileConverter/Views/SettingsWindow.xaml +++ b/Application/FileConverter/Views/SettingsWindow.xaml @@ -127,6 +127,8 @@ + + diff --git a/PresetSamples/ZaidForgeSmartWebpArchive.xml b/PresetSamples/ZaidForgeSmartWebpArchive.xml new file mode 100644 index 00000000..70658c76 --- /dev/null +++ b/PresetSamples/ZaidForgeSmartWebpArchive.xml @@ -0,0 +1,33 @@ + + + + arw + bmp + cr2 + dds + dng + exr + heic + ico + jfif + jpg + jpeg + nef + png + psd + raf + svg + tga + tif + tiff + webp + xcf + None + + + + + + (p:documents)(presetpath)\(sm:yyyy-MM)\(f) + + diff --git a/ZAIDFORGE_VNEXT.md b/ZAIDFORGE_VNEXT.md new file mode 100644 index 00000000..749811a6 --- /dev/null +++ b/ZAIDFORGE_VNEXT.md @@ -0,0 +1,40 @@ +# ZaidForge vNext + +ZaidForge is a proposed vNext identity for a more workflow-focused fork of File Converter. +The name keeps the conversion idea grounded: files go in, useful outputs are forged out, +and the fast Windows Explorer context-menu flow remains the heart of the app. + +## First Principle + +Keep the right-click conversion workflow simple, but make every preset smart enough for +real batches: organized output folders, source-aware names, reliable previews, and fewer +manual cleanup steps after conversion. + +## Seed Feature: Smart Output Templates + +This branch starts by extending output filename templates with tokens that help users +organize conversions automatically: + +- `(preset)` and `(presetname)` insert the selected preset name. +- `(presetpath)` inserts the preset folder path, useful for grouping outputs by workflow. +- `(sc:yyyy-MM-dd)` inserts the source file creation date. +- `(sm:yyyy-MM-dd)` inserts the source file modified date. +- `(n:i:D3)` and `(n:c:D3)` add formatted page or frame counters. + +Example: + +```text +(p:documents)ZaidForge\(presetpath)\(sm:yyyy-MM)\(f) - (preset) +``` + +That can turn a loose folder of media into a date-sorted, preset-sorted output archive +without asking the user to rename files afterward. + +## Next Moves + +- Add a conversion queue history with retry and "open output folder" actions. +- Add a preset pack format for importing and sharing workflow bundles. +- Add a dependency health screen for FFmpeg, ImageMagick, Ghostscript, and Office support. +- Add optional watch folders for automatic conversions. +- Add a modern installer and CI path that can produce signed preview builds. +- Add focused tests around preset serialization, template expansion, and output path safety. From 00cb793e8c89f82043bb298da62b6b8de89aaf29 Mon Sep 17 00:00:00 2001 From: Zaid AlAsali Date: Fri, 5 Jun 2026 17:52:32 +0200 Subject: [PATCH 2/9] fix: harden conversion and shell repair paths --- .github/workflows/build.yml | 24 +++++++ Application/FileConverter/Application.xaml.cs | 18 +++++ .../ConversionJobs/ConversionJob_FFMPEG.cs | 8 ++- Application/FileConverter/Helpers.cs | 70 ++++++++++++++++--- Installer/Installer.wixproj | 4 +- ZAIDFORGE_VNEXT.md | 36 ++++++++-- 6 files changed, 140 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..e5856fdc --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +name: build + +on: + push: + branches: + - integration + - master + - "codex/**" + pull_request: + +jobs: + windows: + name: Windows x64 + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Restore and build + run: msbuild FileConverter.sln /restore /m /p:Configuration=Release /p:Platform=x64 diff --git a/Application/FileConverter/Application.xaml.cs b/Application/FileConverter/Application.xaml.cs index 95f89210..1f7ab2cc 100644 --- a/Application/FileConverter/Application.xaml.cs +++ b/Application/FileConverter/Application.xaml.cs @@ -311,6 +311,24 @@ private void Initialize() return; } + case "repair-shell-extension": + { + string shellExtensionPath = Helpers.GetDefaultShellExtensionPath(); + if (index < args.Length - 1 && !args[index + 1].StartsWith("--")) + { + shellExtensionPath = args[index + 1]; + index++; + } + + if (!Helpers.RepairShellExtension(shellExtensionPath)) + { + Debug.LogError(errorCode: 0x10, $"Failed to repair shell extension {shellExtensionPath}."); + } + + Application.AskForShutdown(); + return; + } + case "version": Console.WriteLine(ApplicationVersion.ToString()); Application.AskForShutdown(); diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs index 79f81586..cd13bd55 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs @@ -79,7 +79,7 @@ protected override void Initialize() { CreateNoWindow = true, UseShellExecute = false, - RedirectStandardOutput = true, + RedirectStandardOutput = false, RedirectStandardError = true }; @@ -88,7 +88,7 @@ protected override void Initialize() protected virtual void FillFFMpegArgumentsList() { - const string baseArgs = "-n -progress pipe:1"; + const string baseArgs = "-n"; bool customCommandEnabled = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.EnableFFMPEGCustomCommand); if (customCommandEnabled) @@ -464,6 +464,10 @@ protected override void Convert() } exeProcess.WaitForExit(); + if (exeProcess.ExitCode != 0 && !this.CancelIsRequested && this.State != ConversionState.Failed) + { + this.ConversionFailed($"FFmpeg exited with code {exeProcess.ExitCode}."); + } } } catch diff --git a/Application/FileConverter/Helpers.cs b/Application/FileConverter/Helpers.cs index 1112e10f..a5f4207e 100644 --- a/Application/FileConverter/Helpers.cs +++ b/Application/FileConverter/Helpers.cs @@ -107,10 +107,10 @@ public static string GetExtensionCategory(string extension) return InputCategoryNames.Misc; } - public static bool RegisterShellExtension(string shellExtensionPath) - { - if (!Application.IsInAdmininstratorPrivileges) - { + public static bool RegisterShellExtension(string shellExtensionPath) + { + if (!Application.IsInAdmininstratorPrivileges) + { Diagnostics.Debug.LogError("File Converter needs administrator privileges to register the shell extension."); return false; } @@ -136,13 +136,61 @@ public static bool RegisterShellExtension(string shellExtensionPath) Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to register."); Diagnostics.Debug.LogError(regasm.StandardError); return false; - } - } - - public static bool UnregisterExtension(string shellExtensionPath) - { - if (!Application.IsInAdmininstratorPrivileges) - { + } + } + + public static string GetDefaultShellExtensionPath() + { + string executablePath = Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path); + string executableFolder = Path.GetDirectoryName(executablePath); + return Path.Combine(executableFolder, "FileConverterExtension.dll"); + } + + public static bool RepairShellExtension(string shellExtensionPath) + { + if (!Application.IsInAdmininstratorPrivileges) + { + Diagnostics.Debug.LogError("File Converter needs administrator privileges to repair the shell extension."); + return false; + } + + if (!File.Exists(shellExtensionPath)) + { + Diagnostics.Debug.LogError($"Shell extension {shellExtensionPath} does not exists."); + return false; + } + + Diagnostics.Debug.Log($"Repair shell extension registration: {shellExtensionPath}."); + + var regasm = new RegAsm(); + if (regasm.Unregister64(shellExtensionPath)) + { + Diagnostics.Debug.Log($"{shellExtensionPath} previous registration removed."); + Diagnostics.Debug.Log(regasm.StandardOutput); + } + else + { + Diagnostics.Debug.Log("Previous shell extension registration could not be removed. Continuing with fresh registration."); + Diagnostics.Debug.Log(regasm.StandardError); + } + + bool success = regasm.Register64(shellExtensionPath, true); + if (success) + { + Diagnostics.Debug.Log($"{shellExtensionPath} repaired and registered."); + Diagnostics.Debug.Log(regasm.StandardOutput); + return true; + } + + Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to register during repair."); + Diagnostics.Debug.LogError(regasm.StandardError); + return false; + } + + public static bool UnregisterExtension(string shellExtensionPath) + { + if (!Application.IsInAdmininstratorPrivileges) + { Diagnostics.Debug.LogError("File Converter needs administrator privileges to unregister the shell extension."); return false; } diff --git a/Installer/Installer.wixproj b/Installer/Installer.wixproj index 463ea144..97aebb8d 100644 --- a/Installer/Installer.wixproj +++ b/Installer/Installer.wixproj @@ -41,5 +41,5 @@ - - \ No newline at end of file + + diff --git a/ZAIDFORGE_VNEXT.md b/ZAIDFORGE_VNEXT.md index 749811a6..ed08dac7 100644 --- a/ZAIDFORGE_VNEXT.md +++ b/ZAIDFORGE_VNEXT.md @@ -10,10 +10,12 @@ Keep the right-click conversion workflow simple, but make every preset smart eno real batches: organized output folders, source-aware names, reliable previews, and fewer manual cleanup steps after conversion. -## Seed Feature: Smart Output Templates +## Seed Improvements -This branch starts by extending output filename templates with tokens that help users -organize conversions automatically: +### Smart Output Templates + +This branch extends output filename templates with tokens that help users organize +conversions automatically: - `(preset)` and `(presetname)` insert the selected preset name. - `(presetpath)` inserts the preset folder path, useful for grouping outputs by workflow. @@ -30,11 +32,35 @@ Example: That can turn a loose folder of media into a date-sorted, preset-sorted output archive without asking the user to rename files afterward. +### Conversion Reliability + +FFmpeg conversions no longer redirect stdout while also asking FFmpeg to write progress +there. The application reads stderr for progress, so leaving stdout redirected could fill +the pipe buffer and hang a conversion. The conversion path now also reports non-zero +FFmpeg exit codes instead of failing silently. + +### Explorer Integration Repair + +The app now has a maintenance command for broken context-menu registrations: + +```text +FileConverter.exe --repair-shell-extension +``` + +Run it as administrator from the install folder to unregister any stale shell extension +registration and register the current `FileConverterExtension.dll` again. + +### Build Health + +A GitHub Actions workflow builds the solution on Windows with MSBuild. The installer +signing import is skipped when the private signing file is absent, so public CI can build +unsigned validation artifacts. + ## Next Moves - Add a conversion queue history with retry and "open output folder" actions. - Add a preset pack format for importing and sharing workflow bundles. - Add a dependency health screen for FFmpeg, ImageMagick, Ghostscript, and Office support. -- Add optional watch folders for automatic conversions. -- Add a modern installer and CI path that can produce signed preview builds. +- Add a small settings button that runs shell-extension repair with elevation. +- Add optional watch folders for automatic conversions once reliability is strong. - Add focused tests around preset serialization, template expansion, and output path safety. From c4cd4e0e8286959e6744677bfff998cba6dbbee1 Mon Sep 17 00:00:00 2001 From: Zaid AlAsali Date: Fri, 5 Jun 2026 18:22:57 +0200 Subject: [PATCH 3/9] fix: polish conversion maintenance edge cases --- Application/FileConverter/Application.xaml.cs | 7 +- .../ConversionJobs/ConversionJob.cs | 90 ++++++++---- .../ConversionJobs/ConversionJobFactory.cs | 3 +- .../ConversionJobs/ConversionJob_Excel.cs | 58 +++++--- .../ConversionJob_ExtractCDA.cs | 131 +++++++++++++----- .../ConversionJobs/ConversionJob_FFMPEG.cs | 12 +- .../ConversionJobs/ConversionJob_Gif.cs | 13 +- .../ConversionJobs/ConversionJob_Ico.cs | 6 +- .../ConversionJob_ImageMagick.cs | 4 +- .../ConversionJob_PowerPoint.cs | 52 ++++--- .../ConversionJobs/ConversionJob_Word.cs | 80 ++++++----- Application/FileConverter/PathHelpers.cs | 25 +++- Application/FileConverter/Registry.cs | 19 +-- .../Services/ConversionService.cs | 2 +- .../FileConverter/Services/UpgradeService.cs | 17 ++- Application/FileConverter/Settings.cs | 45 +++++- .../ViewModels/SettingsViewModel.cs | 39 +++++- .../Resources/ConversionPresetTemplates.xaml | 4 +- .../FileConverterExtension.cs | 114 ++++++++++----- .../FileConverterExtension/PathHelpers.cs | 12 +- ZAIDFORGE_VNEXT.md | 13 ++ 21 files changed, 531 insertions(+), 215 deletions(-) diff --git a/Application/FileConverter/Application.xaml.cs b/Application/FileConverter/Application.xaml.cs index 1f7ab2cc..4560f567 100644 --- a/Application/FileConverter/Application.xaml.cs +++ b/Application/FileConverter/Application.xaml.cs @@ -276,7 +276,8 @@ private void Initialize() if (index >= args.Length - 1) { Debug.LogError(errorCode: 0x0B, $"Invalid format."); - break; + Application.AskForShutdown(); + return; } string shellExtensionPath = args[index + 1]; @@ -296,7 +297,8 @@ private void Initialize() if (index >= args.Length - 1) { Debug.LogError(errorCode: 0x0D, $"Invalid format."); - break; + Application.AskForShutdown(); + return; } string shellExtensionPath = args[index + 1]; @@ -389,6 +391,7 @@ private void Initialize() default: Debug.LogError($"Unknown application argument: '--{parameterTitle}'."); + Application.AskForShutdown(); return; } } diff --git a/Application/FileConverter/ConversionJobs/ConversionJob.cs b/Application/FileConverter/ConversionJobs/ConversionJob.cs index ecf68545..e082cfc7 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob.cs @@ -220,8 +220,14 @@ public void PrepareConversion(params string[] outputFilePaths) this.InputFilePath = this.initialInputPath; - string extension = System.IO.Path.GetExtension(this.initialInputPath); - extension = extension.Substring(1, extension.Length - 1); + string extension = PathHelpers.GetExtensionWithoutDot(this.initialInputPath); + if (string.IsNullOrEmpty(extension)) + { + this.ConversionFailed(Properties.Resources.ErrorInputTypeIncompatibleWithOutputType); + Debug.Log($"Input file has no extension: {this.InputFilePath}."); + return; + } + string extensionCategory = Helpers.GetExtensionCategory(extension); if (!Helpers.IsOutputTypeCompatibleWithCategory(this.ConversionPreset.OutputType, extensionCategory)) { @@ -419,30 +425,43 @@ protected virtual void OnConversionSucceed() this.ChangeOutputFileTimestampToMatchOriginal(); - // Apply the input post conversion action. - switch (this.InputPostConversionAction) + try { - case InputPostConversionAction.None: - break; - - case InputPostConversionAction.MoveInArchiveFolder: - string basePath = System.IO.Path.GetDirectoryName(this.initialInputPath); - string inputFilename = System.IO.Path.GetFileName(this.initialInputPath); - string archivePath = basePath + "\\" + this.ConversionPreset.ConversionArchiveFolderName; - if (!System.IO.Directory.Exists(archivePath)) - { - System.IO.Directory.CreateDirectory(archivePath); - } - - string newPath = PathHelpers.GenerateUniquePath(archivePath + "\\" + inputFilename); - System.IO.File.Move(this.InputFilePath, newPath); - Debug.Log($"Input file moved in archive folder: '{newPath}'"); - break; - - case InputPostConversionAction.Delete: - System.IO.File.Delete(this.InputFilePath); - Debug.Log($"Input file deleted: '{this.initialInputPath}'"); - break; + // Apply the input post conversion action. + switch (this.InputPostConversionAction) + { + case InputPostConversionAction.None: + break; + + case InputPostConversionAction.MoveInArchiveFolder: + string basePath = System.IO.Path.GetDirectoryName(this.initialInputPath); + if (string.IsNullOrEmpty(basePath)) + { + basePath = System.Environment.CurrentDirectory; + } + + string inputFilename = System.IO.Path.GetFileName(this.initialInputPath); + string archivePath = basePath + "\\" + this.ConversionPreset.ConversionArchiveFolderName; + if (!System.IO.Directory.Exists(archivePath)) + { + System.IO.Directory.CreateDirectory(archivePath); + } + + string newPath = PathHelpers.GenerateUniquePath(archivePath + "\\" + inputFilename); + System.IO.File.Move(this.InputFilePath, newPath); + Debug.Log($"Input file moved in archive folder: '{newPath}'"); + break; + + case InputPostConversionAction.Delete: + System.IO.File.Delete(this.InputFilePath); + Debug.Log($"Input file deleted: '{this.initialInputPath}'"); + break; + } + } + catch (Exception exception) + { + this.ConversionFailed($"Post conversion action failed: {exception.Message}"); + return; } Debug.Log(string.Empty); @@ -468,6 +487,27 @@ protected void ConversionFailed(string exitingMessage) this.ErrorMessage = exitingMessage; } + protected void DeleteFileIfExists(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + { + return; + } + + try + { + if (System.IO.File.Exists(filePath)) + { + System.IO.File.Delete(filePath); + } + } + catch (Exception exception) + { + Debug.Log($"Can't delete file '{filePath}'."); + Debug.Log($"An exception has been thrown: {exception}."); + } + } + protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { if (this.PropertyChanged != null) diff --git a/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs b/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs index 34886bb2..52f11b74 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs @@ -6,8 +6,7 @@ public static class ConversionJobFactory { public static ConversionJob Create(ConversionPreset conversionPreset, string inputFilePath) { - string inputFileExtension = System.IO.Path.GetExtension(inputFilePath); - inputFileExtension = inputFileExtension.ToLowerInvariant().Substring(1, inputFileExtension.Length - 1); + string inputFileExtension = PathHelpers.GetExtensionWithoutDot(inputFilePath); if (inputFileExtension == "cda") { return new ConversionJob_ExtractCDA(conversionPreset, inputFilePath); diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_Excel.cs b/Application/FileConverter/ConversionJobs/ConversionJob_Excel.cs index c4d4d021..7ebb8639 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_Excel.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_Excel.cs @@ -48,7 +48,7 @@ protected override int GetOutputFilesCount() Excel.Worksheet worksheet = sheet as Excel.Worksheet; if (worksheet != null) { - pagesCount = worksheet.PageSetup.Pages.Count; + pagesCount += worksheet.PageSetup.Pages.Count; } } @@ -102,19 +102,21 @@ protected override void Convert() return; } - // Make this document the active document. - this.document.Activate(); - - this.UserState = Properties.Resources.ConversionStateConversion; + try + { + // Make this document the active document. + this.document.Activate(); - Debug.Log("Convert excel document to pdf."); - this.document.ExportAsFixedFormat(Excel.Enums.XlFixedFormatType.xlTypePDF, this.intermediateFilePath); + this.UserState = Properties.Resources.ConversionStateConversion; - Debug.Log($"Close excel document '{this.InputFilePath}'."); - this.document.Close(false); - this.document = null; - - this.ReleaseOfficeApplicationInstanceIfNeeded(); + Debug.Log("Convert excel document to pdf."); + this.document.ExportAsFixedFormat(Excel.Enums.XlFixedFormatType.xlTypePDF, this.intermediateFilePath); + } + finally + { + this.CloseDocumentIfNeeded(); + this.ReleaseOfficeApplicationInstanceIfNeeded(); + } if (this.pdf2ImageConversionJob != null) { @@ -140,7 +142,7 @@ protected override void Convert() { Debug.Log($"Delete intermediate file {this.intermediateFilePath}."); - File.Delete(this.intermediateFilePath); + this.DeleteFileIfExists(this.intermediateFilePath); } updateProgress.Wait(); @@ -176,15 +178,11 @@ protected override void ReleaseOfficeApplicationInstanceIfNeeded() private async Task UpdateProgress() { - while (this.pdf2ImageConversionJob.State != ConversionState.Done && + while (this.pdf2ImageConversionJob != null && + this.pdf2ImageConversionJob.State != ConversionState.Done && this.pdf2ImageConversionJob.State != ConversionState.Failed) { - if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress) - { - this.Progress = this.pdf2ImageConversionJob.Progress; - } - - if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress) + if (this.pdf2ImageConversionJob.State == ConversionState.InProgress) { this.Progress = this.pdf2ImageConversionJob.Progress; this.UserState = this.pdf2ImageConversionJob.UserState; @@ -220,5 +218,25 @@ private bool TryLoadDocumentIfNecessary() return this.document != null; } + + private void CloseDocumentIfNeeded() + { + if (this.document == null) + { + return; + } + + try + { + Debug.Log($"Close excel document '{this.InputFilePath}'."); + this.document.Close(false); + } + catch (Exception exception) + { + Debug.Log($"Failed to close excel document '{this.InputFilePath}': {exception.Message}."); + } + + this.document = null; + } } } diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs b/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs index 66c8b381..fd131785 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs @@ -40,7 +40,7 @@ public override void Cancel() { base.Cancel(); - this.compressionConversionJob.Cancel(); + this.compressionConversionJob?.Cancel(); } protected override void Initialize() @@ -111,6 +111,12 @@ protected override void Initialize() // Sub conversion job (for compression). this.compressionConversionJob = ConversionJobFactory.Create(this.ConversionPreset, this.intermediateFilePath); this.compressionConversionJob.PrepareConversion(this.OutputFilePath); + if (this.compressionConversionJob.State == ConversionState.Failed) + { + this.ConversionFailed(this.compressionConversionJob.ErrorMessage); + return; + } + this.compressionThread = Helpers.InstantiateThread("CDACompressionThread", this.CompressAsync); } @@ -125,40 +131,45 @@ protected override void Convert() this.UserState = Properties.Resources.ConversionStateExtraction; - if (!this.diskDrive.IsCDReady()) - { - this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady); - return; - } - - if (!this.diskDrive.Refresh()) - { - Debug.Log("Can't refresh CD drive data."); - this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady); - return; - } - - if (!this.diskDrive.LockCD()) + bool cdLocked = false; + try { - Debug.Log("Can\'t lock cd."); - this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady); - return; + if (!this.diskDrive.IsCDReady()) + { + this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady); + return; + } + + if (!this.diskDrive.Refresh()) + { + Debug.Log("Can't refresh CD drive data."); + this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady); + return; + } + + if (!this.diskDrive.LockCD()) + { + Debug.Log("Can\'t lock cd."); + this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady); + return; + } + + cdLocked = true; + + WaveFormat waveFormat = new WaveFormat(44100, 16, 2); + + using (Stream waveStream = new FileStream(this.intermediateFilePath, FileMode.Create, FileAccess.Write)) + using (this.waveWriter = new WaveWriter(waveStream, waveFormat, this.diskDrive.TrackSize(this.cdaTrackNumber))) + { + this.diskDrive.ReadTrack(this.cdaTrackNumber, this.WriteWaveData, this.CdReadProgress); + } } - - WaveFormat waveFormat = new WaveFormat(44100, 16, 2); - - using (Stream waveStream = new FileStream(this.intermediateFilePath, FileMode.Create, FileAccess.Write)) - using (this.waveWriter = new WaveWriter(waveStream, waveFormat, this.diskDrive.TrackSize(this.cdaTrackNumber))) + finally { - this.diskDrive.ReadTrack(this.cdaTrackNumber, this.WriteWaveData, this.CdReadProgress); + this.waveWriter = null; + this.ReleaseCdDrive(cdLocked); } - this.waveWriter = null; - - this.diskDrive.UnLockCD(); - - this.diskDrive.Close(); - this.StateFlags = ConversionFlags.None; if (!File.Exists(this.intermediateFilePath)) @@ -179,18 +190,21 @@ protected override void Convert() this.compressionConversionJob.State != ConversionState.Failed) { this.Progress = this.compressionConversionJob.Progress; + Thread.Sleep(40); } + this.compressionThread.Join(); + + Debug.Log(string.Empty); + Debug.Log($"Delete intermediate file {this.intermediateFilePath}."); + + this.DeleteIntermediateFileIfExists(); + if (this.compressionConversionJob.State == ConversionState.Failed) { this.ConversionFailed(this.compressionConversionJob.ErrorMessage); return; } - - Debug.Log(string.Empty); - Debug.Log($"Delete intermediate file {this.intermediateFilePath}."); - - File.Delete(this.intermediateFilePath); } private void WriteWaveData(object sender, DataReadEventArgs eventArgs) @@ -220,5 +234,52 @@ private void CompressAsync() { this.compressionConversionJob.StartConversion(); } + + private void ReleaseCdDrive(bool cdLocked) + { + if (this.diskDrive == null) + { + return; + } + + if (cdLocked) + { + try + { + this.diskDrive.UnLockCD(); + } + catch (Exception exception) + { + Debug.Log($"Failed to unlock CD drive: {exception.Message}."); + } + } + + try + { + if (this.diskDrive.IsOpened) + { + this.diskDrive.Close(); + } + } + catch (Exception exception) + { + Debug.Log($"Failed to close CD drive: {exception.Message}."); + } + } + + private void DeleteIntermediateFileIfExists() + { + try + { + if (!string.IsNullOrEmpty(this.intermediateFilePath) && File.Exists(this.intermediateFilePath)) + { + File.Delete(this.intermediateFilePath); + } + } + catch (Exception exception) + { + Debug.Log($"Failed to delete intermediate CDA file {this.intermediateFilePath}: {exception.Message}."); + } + } } } diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs index cd13bd55..bd400539 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs @@ -475,6 +475,11 @@ protected override void Convert() this.ConversionFailed(Properties.Resources.ErrorFailedToLaunchFFMPEG); throw; } + + if (this.State == ConversionState.Failed || this.CancelIsRequested) + { + break; + } } Diagnostics.Debug.Log(string.Empty); @@ -491,12 +496,17 @@ protected override void Convert() Diagnostics.Debug.Log($"Delete intermediate file {currentPass.FileToDelete}."); - File.Delete(currentPass.FileToDelete); + this.DeleteFileIfExists(currentPass.FileToDelete); } } private void ParseFFMPEGOutput(string input) { + if (string.IsNullOrEmpty(input)) + { + return; + } + Match match = this.durationRegex.Match(input); if (match.Success && match.Groups.Count >= 6) { diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs b/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs index bc9fde60..b6d4dab6 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs @@ -20,8 +20,8 @@ public override void Cancel() { base.Cancel(); - this.pngConversionJob.Cancel(); - this.gifConversionJob.Cancel(); + this.pngConversionJob?.Cancel(); + this.gifConversionJob?.Cancel(); } protected override void Initialize() @@ -33,8 +33,7 @@ protected override void Initialize() throw new Exception("The conversion preset must be valid."); } - string extension = System.IO.Path.GetExtension(this.InputFilePath); - extension = extension.ToLowerInvariant().Substring(1, extension.Length - 1); + string extension = PathHelpers.GetExtensionWithoutDot(this.InputFilePath); string inputFilePath = string.Empty; @@ -70,8 +69,6 @@ protected override void Convert() throw new Exception("The conversion preset must be valid."); } - Task updateProgress = this.UpdateProgress(); - if (this.pngConversionJob != null) { this.UserState = Properties.Resources.ConversionStateReadIntputImage; @@ -89,10 +86,12 @@ protected override void Convert() Diagnostics.Debug.Log(string.Empty); Diagnostics.Debug.Log("Convert png intermediate image to gif."); + Task updateProgress = this.UpdateProgress(); this.gifConversionJob.StartConversion(); if (this.gifConversionJob.State != ConversionState.Done) { + updateProgress.Wait(); this.ConversionFailed(this.gifConversionJob.ErrorMessage); return; } @@ -101,7 +100,7 @@ protected override void Convert() { Diagnostics.Debug.Log($"Delete intermediate file {this.intermediateFilePath}."); - File.Delete(this.intermediateFilePath); + this.DeleteFileIfExists(this.intermediateFilePath); } updateProgress.Wait(); diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_Ico.cs b/Application/FileConverter/ConversionJobs/ConversionJob_Ico.cs index 9c6e3231..59c5f99f 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_Ico.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_Ico.cs @@ -19,8 +19,8 @@ public override void Cancel() { base.Cancel(); - this.pngConversionJob.Cancel(); - this.icoConversionJob.Cancel(); + this.pngConversionJob?.Cancel(); + this.icoConversionJob?.Cancel(); } protected override void Initialize() @@ -78,7 +78,7 @@ protected override void Convert() Diagnostics.Debug.Log($"Delete intermediate file {this.intermediateFilePath}."); - File.Delete(this.intermediateFilePath); + this.DeleteFileIfExists(this.intermediateFilePath); } } } diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs b/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs index 312431f1..8d7dea79 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs @@ -46,7 +46,7 @@ protected override int GetOutputFilesCount() { MagickReadSettings settings = new MagickReadSettings(); settings.Density = new Density(1, 1); - images.Read(this.InputFilePath); + images.Read(this.InputFilePath, settings); return images.Count; } @@ -207,7 +207,7 @@ private void ConvertImage(MagickImage image, bool ignoreScale = false) uint width = System.Math.Min(image.Width, maximumSize); uint height = System.Math.Min(image.Height, maximumSize); - Debug.Log($"Clamp size to maximum size of {width}x{width} (from {image.Width}x{image.Height} to {width}x{height})."); + Debug.Log($"Clamp size to maximum size of {width}x{height} (from {image.Width}x{image.Height} to {width}x{height})."); image.Scale(width, height); } diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_PowerPoint.cs b/Application/FileConverter/ConversionJobs/ConversionJob_PowerPoint.cs index 110c7fc7..4f0ddeb5 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_PowerPoint.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_PowerPoint.cs @@ -93,16 +93,18 @@ protected override void Convert() return; } - this.UserState = Properties.Resources.ConversionStateConversion; - - Debug.Log("Convert PowerPoint document to pdf."); - this.document.ExportAsFixedFormat(this.intermediateFilePath, PowerPoint.Enums.PpFixedFormatType.ppFixedFormatTypePDF); - - Debug.Log($"Close PowerPoint document '{this.InputFilePath}'."); - this.document.Close(); - this.document = null; + try + { + this.UserState = Properties.Resources.ConversionStateConversion; - this.ReleaseOfficeApplicationInstanceIfNeeded(); + Debug.Log("Convert PowerPoint document to pdf."); + this.document.ExportAsFixedFormat(this.intermediateFilePath, PowerPoint.Enums.PpFixedFormatType.ppFixedFormatTypePDF); + } + finally + { + this.CloseDocumentIfNeeded(); + this.ReleaseOfficeApplicationInstanceIfNeeded(); + } if (this.pdf2ImageConversionJob != null) { @@ -128,7 +130,7 @@ protected override void Convert() { Debug.Log($"Delete intermediate file {this.intermediateFilePath}."); - File.Delete(this.intermediateFilePath); + this.DeleteFileIfExists(this.intermediateFilePath); } updateProgress.Wait(); @@ -161,15 +163,11 @@ protected override void ReleaseOfficeApplicationInstanceIfNeeded() private async Task UpdateProgress() { - while (this.pdf2ImageConversionJob.State != ConversionState.Done && + while (this.pdf2ImageConversionJob != null && + this.pdf2ImageConversionJob.State != ConversionState.Done && this.pdf2ImageConversionJob.State != ConversionState.Failed) { - if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress) - { - this.Progress = this.pdf2ImageConversionJob.Progress; - } - - if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress) + if (this.pdf2ImageConversionJob.State == ConversionState.InProgress) { this.Progress = this.pdf2ImageConversionJob.Progress; this.UserState = this.pdf2ImageConversionJob.UserState; @@ -205,5 +203,25 @@ private bool TryLoadDocumentIfNecessary() return this.document != null; } + + private void CloseDocumentIfNeeded() + { + if (this.document == null) + { + return; + } + + try + { + Debug.Log($"Close PowerPoint document '{this.InputFilePath}'."); + this.document.Close(); + } + catch (Exception exception) + { + Debug.Log($"Failed to close PowerPoint document '{this.InputFilePath}': {exception.Message}."); + } + + this.document = null; + } } } diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_Word.cs b/Application/FileConverter/ConversionJobs/ConversionJob_Word.cs index cf1360d8..e91258d5 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_Word.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_Word.cs @@ -94,30 +94,32 @@ protected override void Convert() return; } - // Make this document the active document. - this.document.Activate(); - - this.UserState = Properties.Resources.ConversionStateConversion; - - Debug.Log("Convert word document to pdf."); - // this.document.ExportAsFixedFormat(this.intermediateFilePath, Word.WdExportFormat.wdExportFormatPDF); - this.document.ExportAsFixedFormat(this.intermediateFilePath, - Word.Enums.WdExportFormat.wdExportFormatPDF, - false, - Word.Enums.WdExportOptimizeFor.wdExportOptimizeForPrint, - Word.Enums.WdExportRange.wdExportAllDocument, - 1, 1, - Word.Enums.WdExportItem.wdExportDocumentContent, - true, - true, - Word.Enums.WdExportCreateBookmarks.wdExportCreateHeadingBookmarks, - true); - - Debug.Log($"Close word document '{this.InputFilePath}'."); - this.document.Close(Word.Enums.WdSaveOptions.wdDoNotSaveChanges); - this.document = null; + try + { + // Make this document the active document. + this.document.Activate(); + + this.UserState = Properties.Resources.ConversionStateConversion; - this.ReleaseOfficeApplicationInstanceIfNeeded(); + Debug.Log("Convert word document to pdf."); + // this.document.ExportAsFixedFormat(this.intermediateFilePath, Word.WdExportFormat.wdExportFormatPDF); + this.document.ExportAsFixedFormat(this.intermediateFilePath, + Word.Enums.WdExportFormat.wdExportFormatPDF, + false, + Word.Enums.WdExportOptimizeFor.wdExportOptimizeForPrint, + Word.Enums.WdExportRange.wdExportAllDocument, + 1, 1, + Word.Enums.WdExportItem.wdExportDocumentContent, + true, + true, + Word.Enums.WdExportCreateBookmarks.wdExportCreateHeadingBookmarks, + true); + } + finally + { + this.CloseDocumentIfNeeded(); + this.ReleaseOfficeApplicationInstanceIfNeeded(); + } if (this.pdf2ImageConversionJob != null) { @@ -143,7 +145,7 @@ protected override void Convert() { Debug.Log($"Delete intermediate file {this.intermediateFilePath}."); - File.Delete(this.intermediateFilePath); + this.DeleteFileIfExists(this.intermediateFilePath); } updateProgress.Wait(); @@ -179,15 +181,11 @@ protected override void ReleaseOfficeApplicationInstanceIfNeeded() private async Task UpdateProgress() { - while (this.pdf2ImageConversionJob.State != ConversionState.Done && + while (this.pdf2ImageConversionJob != null && + this.pdf2ImageConversionJob.State != ConversionState.Done && this.pdf2ImageConversionJob.State != ConversionState.Failed) { - if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress) - { - this.Progress = this.pdf2ImageConversionJob.Progress; - } - - if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress) + if (this.pdf2ImageConversionJob.State == ConversionState.InProgress) { this.Progress = this.pdf2ImageConversionJob.Progress; this.UserState = this.pdf2ImageConversionJob.UserState; @@ -223,5 +221,25 @@ private bool TryLoadDocumentIfNecessary() return this.document != null; } + + private void CloseDocumentIfNeeded() + { + if (this.document == null) + { + return; + } + + try + { + Debug.Log($"Close word document '{this.InputFilePath}'."); + this.document.Close(Word.Enums.WdSaveOptions.wdDoNotSaveChanges); + } + catch (Exception exception) + { + Debug.Log($"Failed to close word document '{this.InputFilePath}': {exception.Message}."); + } + + this.document = null; + } } } diff --git a/Application/FileConverter/PathHelpers.cs b/Application/FileConverter/PathHelpers.cs index f42acbde..ded6bd79 100644 --- a/Application/FileConverter/PathHelpers.cs +++ b/Application/FileConverter/PathHelpers.cs @@ -67,6 +67,17 @@ public static bool IsPathValid(string path) return PathHelpers.pathRegex.IsMatch(path); } + public static string GetExtensionWithoutDot(string path) + { + string extension = System.IO.Path.GetExtension(path); + if (string.IsNullOrEmpty(extension) || extension.Length <= 1) + { + return string.Empty; + } + + return extension.Substring(1).ToLowerInvariant(); + } + public static string GetFileName(string path) { MatchCollection matchCollection = PathHelpers.filenameRegex.Matches(path); @@ -158,8 +169,13 @@ public static string GenerateFilePathFromTemplate( return "Invalid input file path (argument 0)."; } - string inputExtension = System.IO.Path.GetExtension(inputFilePath).Substring(1); - string inputPathWithoutExtension = inputFilePath.Substring(0, inputFilePath.Length - inputExtension.Length - 1); + string inputExtension = GetExtensionWithoutDot(inputFilePath); + string inputPathWithoutExtension = inputFilePath; + if (!string.IsNullOrEmpty(inputExtension)) + { + inputPathWithoutExtension = inputFilePath.Substring(0, inputFilePath.Length - inputExtension.Length - 1); + } + string outputExtension = outputFileExtension.ToString().ToLowerInvariant(); if (string.IsNullOrEmpty(outputFilePathTemplate)) @@ -170,6 +186,11 @@ public static string GenerateFilePathFromTemplate( string fileName = System.IO.Path.GetFileName(inputPathWithoutExtension); string parentDirectory = System.IO.Path.GetDirectoryName(inputPathWithoutExtension); + if (string.IsNullOrEmpty(parentDirectory)) + { + parentDirectory = System.Environment.CurrentDirectory; + } + if (!parentDirectory.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString())) { parentDirectory += System.IO.Path.DirectorySeparatorChar; diff --git a/Application/FileConverter/Registry.cs b/Application/FileConverter/Registry.cs index 38edb0f0..46d7e0a6 100644 --- a/Application/FileConverter/Registry.cs +++ b/Application/FileConverter/Registry.cs @@ -5,6 +5,7 @@ namespace FileConverter using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Xml.Serialization; public class Registry : IDisposable @@ -24,20 +25,10 @@ public Entry[] SerializableEntries { get { - Entry[] entries = new Entry[this.registryEntries.Count]; - int index = 0; - foreach (KeyValuePair kvp in this.registryEntries) - { - if (kvp.Value == null) - { - continue; - } - - entries[index] = new Entry(kvp.Key, kvp.Value); - index++; - } - - return entries; + return this.registryEntries + .Where(kvp => kvp.Value != null) + .Select(kvp => new Entry(kvp.Key, kvp.Value)) + .ToArray(); } set diff --git a/Application/FileConverter/Services/ConversionService.cs b/Application/FileConverter/Services/ConversionService.cs index 6233d81e..99141a8e 100644 --- a/Application/FileConverter/Services/ConversionService.cs +++ b/Application/FileConverter/Services/ConversionService.cs @@ -38,7 +38,7 @@ public ConversionService(ISettingsService settingsService) if (this.numberOfConversionThread <= 0) { this.numberOfConversionThread = System.Math.Max(1, Environment.ProcessorCount / 2); - Debug.Log($"The number of processors on this computer is {settingsService.Settings.MaximumNumberOfSimultaneousConversions}. Set the default number of conversion threads to {settingsService.Settings.MaximumNumberOfSimultaneousConversions}"); + Debug.Log($"The number of processors on this computer is {Environment.ProcessorCount}. Set the default number of conversion threads to {this.numberOfConversionThread}"); } } diff --git a/Application/FileConverter/Services/UpgradeService.cs b/Application/FileConverter/Services/UpgradeService.cs index 1ff083dd..f0fb8600 100644 --- a/Application/FileConverter/Services/UpgradeService.cs +++ b/Application/FileConverter/Services/UpgradeService.cs @@ -68,6 +68,11 @@ public async Task CheckForUpgrade() Diagnostics.Debug.Log($"Failed to check upgrade: {exception.Message}."); } + if (task == null) + { + return null; + } + UpgradeVersionDescription versionDescription = await task; if (versionDescription == null) @@ -155,7 +160,7 @@ public void CancelUpgrade() private async Task DownloadLatestVersionDescription() { #if BUILD32 - Uri uri = new Uri(Helpers.BaseURI + "version (x86).xml"); + Uri uri = new Uri(UpgradeService.BaseURI + "version (x86).xml"); #else Uri uri = new Uri(UpgradeService.BaseURI + "version.xml"); #endif @@ -185,7 +190,7 @@ private async Task DownloadLatestVersionDescription() } catch (Exception) { - Debug.Log("Error while retrieving change log."); + Debug.Log("Error while retrieving version description."); return null; } @@ -236,12 +241,13 @@ private async Task DownloadInstaller() this.UpgradeVersionDescription.InstallerDownloadProgress = 100; this.UpgradeVersionDescription.InstallerDownloadInProgress = false; - this.UpgradeVersionDescription = null; } catch (Exception exception) { Debug.LogError("Failed to download the new File Converter upgrade. You should try again or download it manually."); Debug.Log(exception.ToString()); + this.UpgradeVersionDescription.InstallerDownloadInProgress = false; + this.UpgradeVersionDescription.InstallerDownloadProgress = 0; this.UpgradeVersionDescription.NeedToUpgrade = false; } @@ -250,6 +256,11 @@ private async Task DownloadInstaller() private void WebClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs eventArgs) { + if (this.UpgradeVersionDescription == null) + { + return; + } + this.UpgradeVersionDescription.InstallerDownloadProgress = eventArgs.ProgressPercentage; } } diff --git a/Application/FileConverter/Settings.cs b/Application/FileConverter/Settings.cs index 84e8f1b2..88eb759a 100644 --- a/Application/FileConverter/Settings.cs +++ b/Application/FileConverter/Settings.cs @@ -26,13 +26,19 @@ public class Settings : ObservableObject, IXmlSerializable public ConversionPreset GetPresetFromName(string presetName) { - return this.conversionPresets.FirstOrDefault(match => match.FullName == presetName); + return this.conversionPresets.FirstOrDefault(match => match != null && match.FullName == presetName); } public void Clean() { - for (int index = 0; index < this.ConversionPresets.Count; index++) + for (int index = this.ConversionPresets.Count - 1; index >= 0; index--) { + if (this.ConversionPresets[index] == null) + { + this.ConversionPresets.RemoveAt(index); + continue; + } + this.ConversionPresets[index].Clean(); } } @@ -47,7 +53,12 @@ public Settings Merge(Settings settings) for (int index = 0; index < settings.conversionPresets.Count; index++) { ConversionPreset conversionPreset = settings.conversionPresets[index]; - if (this.conversionPresets.Any(match => match.FullName == conversionPreset.FullName)) + if (conversionPreset == null) + { + continue; + } + + if (this.conversionPresets.Any(match => match != null && match.FullName == conversionPreset.FullName)) { continue; } @@ -112,7 +123,15 @@ public string ApplicationLanguageName return; } - this.ApplicationLanguage = CultureInfo.GetCultureInfo(value); + try + { + this.ApplicationLanguage = CultureInfo.GetCultureInfo(value); + } + catch (CultureNotFoundException) + { + Diagnostics.Debug.Log($"Unsupported application language '{value}'. Fallback to default culture."); + this.ApplicationLanguage = null; + } } } @@ -186,8 +205,18 @@ public ConversionPreset[] SerializableConversionPresets set { + if (value == null) + { + return; + } + for (int index = 0; index < value.Length; index++) { + if (value[index] == null) + { + continue; + } + this.ConversionPresets.Add(value[index]); } } @@ -241,8 +270,14 @@ public void OnDeserializationComplete() { this.DurationBetweenEndOfConversionsAndApplicationExit = System.Math.Max(0, System.Math.Min(10, this.DurationBetweenEndOfConversionsAndApplicationExit)); - for (int index = 0; index < this.ConversionPresets.Count; index++) + for (int index = this.ConversionPresets.Count - 1; index >= 0; index--) { + if (this.ConversionPresets[index] == null) + { + this.ConversionPresets.RemoveAt(index); + continue; + } + this.ConversionPresets[index].OnDeserializationComplete(); } diff --git a/Application/FileConverter/ViewModels/SettingsViewModel.cs b/Application/FileConverter/ViewModels/SettingsViewModel.cs index 15d33c87..fd610ac9 100644 --- a/Application/FileConverter/ViewModels/SettingsViewModel.cs +++ b/Application/FileConverter/ViewModels/SettingsViewModel.cs @@ -58,7 +58,7 @@ public class SettingsViewModel : ObservableRecipient, IDataErrorInfo public SettingsViewModel() { this.getChangeLogContentCommand = new RelayCommand(this.DownloadChangeLogAction); - this.openUrlCommand = new RelayCommand((url) => Process.Start(url)); + this.openUrlCommand = new RelayCommand(this.OpenUrl); this.createFolderCommand = new RelayCommand(this.CreateFolder); this.newPresetCommand = new RelayCommand(() => this.AddNewPreset(false)); this.duplicatePresetCommand = new RelayCommand(() => this.AddNewPreset(true), this.CanDuplicateSelectedPreset); @@ -84,6 +84,7 @@ public SettingsViewModel() outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Avi)); outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Png)); outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Jpg)); + outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Avif)); outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Webp)); outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Ico)); outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Gif)); @@ -387,6 +388,23 @@ private void DownloadChangeLogAction() this.DisplaySeeChangeLogLink = false; } + private void OpenUrl(string url) + { + if (string.IsNullOrEmpty(url)) + { + return; + } + + try + { + Process.Start(url); + } + catch (Exception exception) + { + Diagnostics.Debug.Log($"Failed to open URL '{url}': {exception.Message}."); + } + } + private void InitializeCompatibleInputExtensions() { List categories = new List(); @@ -526,7 +544,7 @@ private void CreateFolder() this.saveCommand.NotifyCanExecuteChanged(); - this.OnFolderCreated(); + this.OnFolderCreated?.Invoke(); } private bool CanDuplicateSelectedPreset() @@ -584,7 +602,7 @@ private void AddNewPreset(bool duplicate) this.SelectedItem = node; - this.OnPresetCreated.Invoke(); + this.OnPresetCreated?.Invoke(); this.removePresetCommand.NotifyCanExecuteChanged(); this.saveCommand.NotifyCanExecuteChanged(); @@ -604,6 +622,7 @@ private void ImportPreset() if (!File.Exists(openFileDialog.FileName)) { Diagnostics.Debug.LogError("File does not exists."); + return; } string directoryPath = Path.GetDirectoryName(openFileDialog.FileName); @@ -613,7 +632,15 @@ private void ImportPreset() } List presetsToImport = new List(); - XmlHelpers.LoadFromFile("Presets", openFileDialog.FileName, out presetsToImport); + try + { + XmlHelpers.LoadFromFile("Presets", openFileDialog.FileName, out presetsToImport); + } + catch (Exception exception) + { + Diagnostics.Debug.LogError($"Failed to import presets. {exception.Message}"); + return; + } // Add imported preset to preset tree. bool itemSelected = false; @@ -644,6 +671,8 @@ private void ImportPreset() itemSelected = true; } } + + this.saveCommand.NotifyCanExecuteChanged(); } } @@ -699,7 +728,7 @@ private void RemoveSelectedPreset() private bool CanRemoveSelectedPreset() { - return this.SelectedItem != null; + return this.SelectedItem != null && this.SelectedItem.Parent != null; } protected override void OnDeactivated() diff --git a/Application/FileConverter/Views/Resources/ConversionPresetTemplates.xaml b/Application/FileConverter/Views/Resources/ConversionPresetTemplates.xaml index 98ddf1e7..0b2a1125 100644 --- a/Application/FileConverter/Views/Resources/ConversionPresetTemplates.xaml +++ b/Application/FileConverter/Views/Resources/ConversionPresetTemplates.xaml @@ -624,6 +624,9 @@ + + + @@ -710,4 +713,3 @@ - \ No newline at end of file diff --git a/Application/FileConverterExtension/FileConverterExtension.cs b/Application/FileConverterExtension/FileConverterExtension.cs index 8ce6f859..62694515 100644 --- a/Application/FileConverterExtension/FileConverterExtension.cs +++ b/Application/FileConverterExtension/FileConverterExtension.cs @@ -2,6 +2,7 @@ namespace FileConverterExtension { + using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; @@ -46,7 +47,13 @@ private bool DisplayPresetIcons { get { - string displayPresetIcons = PathHelpers.FileConverterRegistryKey.GetValue("DisplayPresetIcons") as string; + var registryKey = PathHelpers.FileConverterRegistryKey; + if (registryKey == null) + { + return false; + } + + string displayPresetIcons = registryKey.GetValue("DisplayPresetIcons") as string; if (displayPresetIcons == null) { return false; @@ -67,7 +74,7 @@ private PresetReference[] PresetReferences { this.LoadExtensionSettingsIfNecessary(); - return this.presetReferences; + return this.presetReferences ?? new PresetReference[0]; } } @@ -80,7 +87,7 @@ protected override bool CanShowMenu() { foreach (PresetReference presetReference in presets) { - if (presetReference.InputTypes.Contains(extension)) + if (presetReference.InputTypes != null && presetReference.InputTypes.Contains(extension)) { return true; } @@ -209,13 +216,13 @@ private void RefreshPresetList() this.RefreshExtensionCacheFromSelectedItems(); // Activate compatible menu entries. - PresetReference[] presets = this.presetReferences; + PresetReference[] presets = this.PresetReferences; this.menuEntries.Clear(); foreach (string extension in this.extensionCache) { foreach (PresetReference presetReference in presets) { - if (!presetReference.InputTypes.Contains(extension)) + if (presetReference.InputTypes == null || !presetReference.InputTypes.Contains(extension)) { continue; } @@ -270,19 +277,13 @@ private void LoadExtensionSettingsIfNecessary() private void OpenSettings() { - if (string.IsNullOrEmpty(PathHelpers.FileConverterPath)) + string fileConverterPath = this.GetFileConverterPathOrShowError(); + if (string.IsNullOrEmpty(fileConverterPath)) { - MessageBox.Show("Can't retrieve the file converter executable path. You should try to reinstall the application."); return; } - if (!File.Exists(PathHelpers.FileConverterPath)) - { - MessageBox.Show($"Can't find the file converter executable ({PathHelpers.FileConverterPath}). You should try to reinstall the application."); - return; - } - - ProcessStartInfo processStartInfo = new ProcessStartInfo(PathHelpers.FileConverterPath) + ProcessStartInfo processStartInfo = new ProcessStartInfo(fileConverterPath) { CreateNoWindow = false, UseShellExecute = false, @@ -294,20 +295,14 @@ private void OpenSettings() stringBuilder.Append("--settings"); processStartInfo.Arguments = stringBuilder.ToString(); - Process exeProcess = Process.Start(processStartInfo); + this.TryStartFileConverter(processStartInfo, null); } private void ConvertFiles(string presetName) { - if (string.IsNullOrEmpty(PathHelpers.FileConverterPath)) - { - MessageBox.Show("Can't retrieve the file converter executable path. You should try to reinstall the application."); - return; - } - - if (!File.Exists(PathHelpers.FileConverterPath)) + string fileConverterPath = this.GetFileConverterPathOrShowError(); + if (string.IsNullOrEmpty(fileConverterPath)) { - MessageBox.Show($"Can't find the file converter executable ({PathHelpers.FileConverterPath}). You should try to reinstall the application."); return; } @@ -362,7 +357,7 @@ void BuildConversionPresetArgument(StringBuilder sb) } } - var processStartInfo = new ProcessStartInfo(PathHelpers.FileConverterPath) + var processStartInfo = new ProcessStartInfo(fileConverterPath) { CreateNoWindow = false, UseShellExecute = false, @@ -370,21 +365,72 @@ void BuildConversionPresetArgument(StringBuilder sb) Arguments = stringBuilder.ToString(), }; - Process exeProcess = Process.Start(processStartInfo); + Process exeProcess = this.TryStartFileConverter(processStartInfo, fileListPath); + if (exeProcess == null) + { + return; + } + exeProcess.EnableRaisingEvents = true; exeProcess.Exited += (sender, args) => { - if (fileListPath != null) + DeleteInputListFile(fileListPath); + }; + } + + private string GetFileConverterPathOrShowError() + { + string fileConverterPath = PathHelpers.FileConverterPath; + if (string.IsNullOrEmpty(fileConverterPath)) + { + MessageBox.Show("Can't retrieve the file converter executable path. You should try to reinstall the application."); + return null; + } + + if (!File.Exists(fileConverterPath)) + { + MessageBox.Show($"Can't find the file converter executable ({fileConverterPath}). You should try to reinstall the application."); + return null; + } + + return fileConverterPath; + } + + private Process TryStartFileConverter(ProcessStartInfo processStartInfo, string temporaryInputListPath) + { + try + { + Process process = Process.Start(processStartInfo); + if (process != null) { - try - { - File.Delete(fileListPath); - } - catch - { - } + return process; } - }; + + MessageBox.Show("Failed to start File Converter."); + } + catch (Exception exception) + { + MessageBox.Show($"Failed to start File Converter. {exception.Message}"); + } + + DeleteInputListFile(temporaryInputListPath); + return null; + } + + private static void DeleteInputListFile(string fileListPath) + { + if (fileListPath == null) + { + return; + } + + try + { + File.Delete(fileListPath); + } + catch + { + } } } } diff --git a/Application/FileConverterExtension/PathHelpers.cs b/Application/FileConverterExtension/PathHelpers.cs index 21d66f26..c4046a75 100644 --- a/Application/FileConverterExtension/PathHelpers.cs +++ b/Application/FileConverterExtension/PathHelpers.cs @@ -34,10 +34,6 @@ public static RegistryKey FileConverterRegistryKey if (PathHelpers.fileConverterRegistryKey == null) { PathHelpers.fileConverterRegistryKey = Registry.CurrentUser.OpenSubKey(@"Software\FileConverter"); - if (PathHelpers.fileConverterRegistryKey == null) - { - throw new Exception("Can't retrieve file converter registry entry."); - } } return PathHelpers.fileConverterRegistryKey; @@ -50,7 +46,13 @@ public static string FileConverterPath { if (string.IsNullOrEmpty(PathHelpers.fileConverterPath)) { - PathHelpers.fileConverterPath = PathHelpers.FileConverterRegistryKey.GetValue("Path") as string; + RegistryKey registryKey = PathHelpers.FileConverterRegistryKey; + if (registryKey == null) + { + return null; + } + + PathHelpers.fileConverterPath = registryKey.GetValue("Path") as string; } return PathHelpers.fileConverterPath; diff --git a/ZAIDFORGE_VNEXT.md b/ZAIDFORGE_VNEXT.md index ed08dac7..3c448168 100644 --- a/ZAIDFORGE_VNEXT.md +++ b/ZAIDFORGE_VNEXT.md @@ -56,6 +56,19 @@ A GitHub Actions workflow builds the solution on Windows with MSBuild. The insta signing import is skipped when the private signing file is absent, so public CI can build unsigned validation artifacts. +### Maintenance Audit Polish + +The first audit pass focuses on boring, valuable things that make an abandoned utility +feel maintained again: + +- No-extension inputs now fail gracefully instead of throwing during preparation. +- Office conversions close Word, Excel, and PowerPoint even when PDF export fails. +- CDA extraction unlocks and closes the drive on early failures and cleans temporary WAVs. +- FFmpeg multi-pass jobs stop after a failed pass and clean intermediate files defensively. +- Explorer extension startup failures now show useful messages and clean temp input lists. +- Settings, registry, imported presets, and language values tolerate malformed user data. +- AVIF appears in the output-type picker and uses the standard image quality controls. + ## Next Moves - Add a conversion queue history with retry and "open output folder" actions. From e1a3fa02e2375fe4df3f05c13f4bc4aaf26f70c7 Mon Sep 17 00:00:00 2001 From: Zaid AlAsali Date: Fri, 5 Jun 2026 19:04:49 +0200 Subject: [PATCH 4/9] feat: brand ZFileConverter and add maintenance polish --- .editorconfig | 18 + .gitattributes | 20 + .github/PULL_REQUEST_TEMPLATE.md | 19 + .github/workflows/build.yml | 23 +- Application/FileConverter/Application.xaml.cs | 10 +- .../Controls/ConversionJobControl.xaml | 32 +- .../ConversionJobs/ConversionJob.cs | 90 ++ .../FileConverter/Diagnostics/Debug.cs | 12 + .../FileConverter/FileConverter.csproj | 852 ++++++------ Application/FileConverter/Helpers.cs | 688 +++++----- .../FileConverter/Properties/AssemblyInfo.cs | 10 +- .../Properties/Resources.en.resx | 22 +- .../FileConverter/Properties/Resources.resx | 1213 ++++++++--------- .../ConversionJobRegisteredEventArgs.cs | 21 + .../Services/ConversionService.cs | 182 ++- .../Services/IConversionService.cs | 4 + .../FileConverter/Services/UpgradeService.cs | 8 +- .../ApplicationVersionToApplicationName.cs | 4 +- .../ViewModels/DependencyStatusViewModel.cs | 46 + .../ViewModels/DiagnosticsViewModel.cs | 62 + .../FileConverter/ViewModels/MainViewModel.cs | 24 +- .../ViewModels/SettingsViewModel.cs | 191 ++- .../Views/DiagnosticsWindow.xaml | 4 + .../FileConverter/Views/SettingsWindow.xaml | 83 +- .../FileConverterExtension.cs | 10 +- Installer/DebugInstaller.bat | 12 +- Installer/Installer.wixproj | 2 +- Installer/Product.wxs | 10 +- README.md | 131 +- RELEASE_CHECKLIST.md | 52 + ZAIDFORGE_VNEXT.md | 79 -- ZFILECONVERTER_ROADMAP.md | 37 + docs/BUILDING.md | 57 + version (x86).xml | 2 +- version.xml | 2 +- 35 files changed, 2369 insertions(+), 1663 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 Application/FileConverter/Services/ConversionJobRegisteredEventArgs.cs create mode 100644 Application/FileConverter/ViewModels/DependencyStatusViewModel.cs create mode 100644 RELEASE_CHECKLIST.md delete mode 100644 ZAIDFORGE_VNEXT.md create mode 100644 ZFILECONVERTER_ROADMAP.md create mode 100644 docs/BUILDING.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..242a558a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{cs,xaml,csproj,wixproj,wxs,resx,xml}] +indent_style = space +indent_size = 4 + +[*.{md,yml,yaml}] +indent_style = space +indent_size = 2 + +[*.bat] +indent_style = space +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6abe7d41 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,20 @@ +* text=auto + +*.bat text eol=crlf +*.cs text eol=crlf +*.csproj text eol=crlf +*.resx text eol=crlf +*.wixproj text eol=crlf +*.wxs text eol=crlf +*.xaml text eol=crlf +*.xml text eol=crlf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf + +*.bmp binary +*.gif binary +*.ico binary +*.jpg binary +*.png binary +*.rtf binary diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..315dfbd9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +## Summary + + + +## User Impact + + + +## Validation + +- [ ] `git diff --check` +- [ ] XML/XAML/project files parse +- [ ] Release x64 build +- [ ] Smoke conversion checked +- [ ] Installer or Explorer integration checked when relevant + +## Notes + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5856fdc..788e37a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,13 @@ on: - "codex/**" pull_request: +permissions: + contents: read + +env: + BUILD_CONFIGURATION: Release + BUILD_PLATFORM: x64 + jobs: windows: name: Windows x64 @@ -21,4 +28,18 @@ jobs: uses: microsoft/setup-msbuild@v2 - name: Restore and build - run: msbuild FileConverter.sln /restore /m /p:Configuration=Release /p:Platform=x64 + run: msbuild FileConverter.sln /restore /m /p:Configuration=${{ env.BUILD_CONFIGURATION }} /p:Platform=${{ env.BUILD_PLATFORM }} + + - name: Upload app artifact + uses: actions/upload-artifact@v4 + with: + name: ZFileConverter-app-x64 + path: Application/FileConverter/bin/x64/Release/** + if-no-files-found: error + + - name: Upload installer artifact + uses: actions/upload-artifact@v4 + with: + name: ZFileConverter-installer-x64 + path: Installer/bin/x64/Release/*.msi + if-no-files-found: warn diff --git a/Application/FileConverter/Application.xaml.cs b/Application/FileConverter/Application.xaml.cs index 4560f567..cc4472f9 100644 --- a/Application/FileConverter/Application.xaml.cs +++ b/Application/FileConverter/Application.xaml.cs @@ -1,7 +1,7 @@ // License: http://www.gnu.org/licenses/gpl.html GPL version 3. -/* File Converter - This program allow you to convert file format to another. - Copyright (C) 2026 Adrien Allard +/* ZFileConverter - This program allows you to convert one file format to another. + Copyright (C) 2026 ZaidNAlAsali and File Converter contributors email: adrien.allard.pro@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -218,9 +218,9 @@ private void RegisterServices() private void Initialize() { #if BUILD32 - Diagnostics.Debug.Log("File Converter v" + ApplicationVersion.ToString() + " (32 bits)"); + Diagnostics.Debug.Log("ZFileConverter v" + ApplicationVersion.ToString() + " (32 bits)"); #else - Diagnostics.Debug.Log("File Converter v" + ApplicationVersion.ToString() + " (64 bits)"); + Diagnostics.Debug.Log("ZFileConverter v" + ApplicationVersion.ToString() + " (64 bits)"); #endif // Retrieve arguments. @@ -409,7 +409,7 @@ private void RunConversions(List filePaths, string conversionPresetName) ISettingsService settingsService = Ioc.Default.GetRequiredService(); if (settingsService.Settings == null) { - Debug.LogError(errorCode: 0x04, "Can't load File Converter settings. The application will now shutdown, if you want to fix the problem yourself please edit or delete the file: C:\\Users\\UserName\\AppData\\Local\\FileConverter\\Settings.user.xml."); + Debug.LogError(errorCode: 0x04, "Can't load ZFileConverter settings. The application will now shutdown, if you want to fix the problem yourself please edit or delete the file: C:\\Users\\UserName\\AppData\\Local\\FileConverter\\Settings.user.xml."); Application.AskForShutdown(); return; } diff --git a/Application/FileConverter/Controls/ConversionJobControl.xaml b/Application/FileConverter/Controls/ConversionJobControl.xaml index caed4eb7..3976fd39 100644 --- a/Application/FileConverter/Controls/ConversionJobControl.xaml +++ b/Application/FileConverter/Controls/ConversionJobControl.xaml @@ -15,7 +15,7 @@ - + @@ -54,15 +54,27 @@ - - - + + + + + + + diff --git a/Application/FileConverter/ConversionJobs/ConversionJob.cs b/Application/FileConverter/ConversionJobs/ConversionJob.cs index e082cfc7..cf06eaa9 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob.cs @@ -4,12 +4,15 @@ namespace FileConverter.ConversionJobs { using System; using System.ComponentModel; + using System.IO; using System.Runtime.CompilerServices; using System.Windows.Input; + using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; using FileConverter.Diagnostics; + using FileConverter.Services; public class ConversionJob : INotifyPropertyChanged { @@ -19,6 +22,8 @@ public class ConversionJob : INotifyPropertyChanged private string errorMessage = string.Empty; private string userState = string.Empty; private RelayCommand cancelCommand; + private RelayCommand openOutputFolderCommand; + private RelayCommand retryCommand; private readonly string initialInputPath; private int currentOutputFilePathIndex; @@ -64,6 +69,8 @@ public ConversionPreset ConversionPreset private set; } + public string InitialInputPath => this.initialInputPath; + public string InputFilePath { get; @@ -102,6 +109,8 @@ private set this.state = value; this.NotifyPropertyChanged(); Application.Current.Dispatcher.Invoke(() => this.cancelCommand?.NotifyCanExecuteChanged()); + Application.Current.Dispatcher.Invoke(() => this.openOutputFolderCommand?.NotifyCanExecuteChanged()); + Application.Current.Dispatcher.Invoke(() => this.retryCommand?.NotifyCanExecuteChanged()); } } @@ -168,6 +177,32 @@ public ICommand CancelCommand } } + public ICommand OpenOutputFolderCommand + { + get + { + if (this.openOutputFolderCommand == null) + { + this.openOutputFolderCommand = new RelayCommand(this.OpenOutputFolder, this.CanOpenOutputFolder); + } + + return this.openOutputFolderCommand; + } + } + + public ICommand RetryCommand + { + get + { + if (this.retryCommand == null) + { + this.retryCommand = new RelayCommand(this.RetryConversion, this.CanRetryConversion); + } + + return this.retryCommand; + } + } + protected bool CancelIsRequested { get; @@ -200,6 +235,18 @@ protected virtual InputPostConversionAction InputPostConversionAction protected virtual bool IsCancelable() => this.State == ConversionState.InProgress; + private bool CanOpenOutputFolder() + { + return this.State == ConversionState.Done && !string.IsNullOrEmpty(this.OutputFilePath); + } + + private bool CanRetryConversion() + { + return this.State == ConversionState.Failed && + this.ConversionPreset != null && + !string.IsNullOrEmpty(this.initialInputPath); + } + protected string[] OutputFilePaths { get; @@ -384,6 +431,49 @@ public virtual void Cancel() this.ConversionFailed(Properties.Resources.ErrorCanceled); } + private void OpenOutputFolder() + { + if (string.IsNullOrEmpty(this.OutputFilePath)) + { + return; + } + + try + { + string outputFilePath = this.OutputFilePath; + if (File.Exists(outputFilePath)) + { + System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{outputFilePath}\""); + return; + } + + string outputDirectory = Path.GetDirectoryName(outputFilePath); + if (!string.IsNullOrEmpty(outputDirectory) && Directory.Exists(outputDirectory)) + { + System.Diagnostics.Process.Start("explorer.exe", $"\"{outputDirectory}\""); + return; + } + + Debug.Log($"Can't open output folder because the path does not exist: {outputFilePath}."); + } + catch (Exception exception) + { + Debug.Log($"Can't open output folder: {exception.Message}."); + } + } + + private void RetryConversion() + { + try + { + Ioc.Default.GetRequiredService().RetryConversionJob(this); + } + catch (Exception exception) + { + Debug.Log($"Can't retry conversion: {exception.Message}."); + } + } + protected virtual int GetOutputFilesCount() { return 1; diff --git a/Application/FileConverter/Diagnostics/Debug.cs b/Application/FileConverter/Diagnostics/Debug.cs index 6dda0063..42e21dc5 100644 --- a/Application/FileConverter/Diagnostics/Debug.cs +++ b/Application/FileConverter/Diagnostics/Debug.cs @@ -56,6 +56,18 @@ public static int FirstErrorCode public static DiagnosticsData[] Data => Debug.diagnosticsDataById.Values.ToArray(); + public static string DiagnosticsFolderPath => Debug.diagnosticsFolderPath; + + public static string AllContent + { + get + { + return string.Join( + Environment.NewLine + Environment.NewLine, + Debug.Data.Select(data => $"[{data.Name}]{Environment.NewLine}{data.Content}")); + } + } + public static void Log(string message) { Debug.LogInternal(error: false, message, ConsoleColor.White); diff --git a/Application/FileConverter/FileConverter.csproj b/Application/FileConverter/FileConverter.csproj index fd50ac17..de59ad4b 100644 --- a/Application/FileConverter/FileConverter.csproj +++ b/Application/FileConverter/FileConverter.csproj @@ -1,424 +1,426 @@ - - - - - Debug - AnyCPU - {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5} - WinExe - Properties - FileConverter - FileConverter - v4.8 - 512 - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 4 - - - - - - AnyCPU - true - full - false - bin\Debug\ - TRACE;DEBUG - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - x64 - bin\x64\Debug\ - TRACE;DEBUG - - - x64 - bin\x64\Release\ - - - Resources\ApplicationIcon.ico - - - true - bin\x86\Debug\ - TRACE;DEBUG;BUILD32 - full - x86 - prompt - MinimumRecommendedRules.ruleset - true - - - bin\x86\Release\ - TRACE;BUILD32 - true - pdbonly - x86 - prompt - MinimumRecommendedRules.ruleset - true - - - - ..\..\Middleware\Markdown.Xaml.dll - - - - False - ..\..\Middleware\Ripper.dll - - - - - - - - - - - - 4.0 - - - - - - - - ..\..\Middleware\yeti.mmedia.dll - - - - - MSBuild:Compile - Designer - - - - ConversionJobControl.xaml - - - - EncodingQualitySliderControl.xaml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Resources.en.resx - True - True - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - HelpWindow.xaml - - - DiagnosticsWindow.xaml - - - - - - SettingsWindow.xaml - - - - UpgradeWindow.xaml - - - - Designer - MSBuild:Compile - - - Designer - MSBuild:Compile - - - Designer - MSBuild:Compile - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - Application.xaml - Code - - - - - - MainWindow.xaml - Code - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - Designer - MSBuild:Compile - - - Designer - MSBuild:Compile - - - - - - true - - - Code - true - - - True - True - Resources.resx - - - True - Settings.settings - True - - - - - - - - - - - PublicResXFileCodeGenerator - Resources.en.Designer.cs - Designer - - - - - - - - - - - - PublicResXFileCodeGenerator - Resources.Designer.cs - Designer - - - - - - - - - - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {0c44ca69-42d6-4357-bdfd-83069d1aba2f} - FileConverterExtension - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 8.4.0 - - - 14.10.2 - - - 10.0.3 - - - 1.1.135 - - - 1.7.4.11 - - - 1.7.4.11 - - - 1.7.4.11 - - - 2.7.2 - - - 2.0.2 - - - - + + + + + Debug + AnyCPU + {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5} + WinExe + Properties + FileConverter + FileConverter + v4.8 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + + + + + AnyCPU + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + x64 + bin\x64\Debug\ + TRACE;DEBUG + + + x64 + bin\x64\Release\ + + + Resources\ApplicationIcon.ico + + + true + bin\x86\Debug\ + TRACE;DEBUG;BUILD32 + full + x86 + prompt + MinimumRecommendedRules.ruleset + true + + + bin\x86\Release\ + TRACE;BUILD32 + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + true + + + + ..\..\Middleware\Markdown.Xaml.dll + + + + False + ..\..\Middleware\Ripper.dll + + + + + + + + + + + + 4.0 + + + + + + + + ..\..\Middleware\yeti.mmedia.dll + + + + + MSBuild:Compile + Designer + + + + ConversionJobControl.xaml + + + + EncodingQualitySliderControl.xaml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Resources.en.resx + True + True + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HelpWindow.xaml + + + DiagnosticsWindow.xaml + + + + + + SettingsWindow.xaml + + + + UpgradeWindow.xaml + + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + Application.xaml + Code + + + + + + MainWindow.xaml + Code + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + + true + + + Code + true + + + True + True + Resources.resx + + + True + Settings.settings + True + + + + + + + + + + + PublicResXFileCodeGenerator + Resources.en.Designer.cs + Designer + + + + + + + + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {0c44ca69-42d6-4357-bdfd-83069d1aba2f} + FileConverterExtension + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8.4.0 + + + 14.10.2 + + + 10.0.3 + + + 1.1.135 + + + 1.7.4.11 + + + 1.7.4.11 + + + 1.7.4.11 + + + 2.7.2 + + + 2.0.2 + + + + copy /Y "$(SolutionDir)Middleware\ffmpeg\ffmpeg.exe" "$(TargetDir)ffmpeg.exe" copy /Y "$(SolutionDir)Middleware\gs\gsdll64.dll" "$(TargetDir)gsdll64.dll" copy /Y "$(SolutionDir)Middleware\gs\gswin64c.exe" "$(TargetDir)gswin64c.exe" @@ -426,13 +428,13 @@ copy /Y "$(ProjectDir)Settings.default.xml" "$(TargetDir)Settings.default.xml" robocopy $(TargetDir) $(TargetDir)\Languages "$(TargetName).resources.dll" /CREATE /S /XD Languages /IS /IT /NFL /NDL /NJH /NJS /NC /NS /NP if %25errorlevel%25 leq 1 exit 0 else exit %25errorlevel%25 robocopy $(TargetDir) $(TargetDir)\Languages "$(TargetName).resources.dll" /MOVE /S /XD Languages /XL /IS /IT /NFL /NDL /NJH /NJS /NC /NS /NP -if %25errorlevel%25 leq 1 exit 0 else exit %25errorlevel%25 - +if %25errorlevel%25 leq 1 exit 0 else exit %25errorlevel%25 + - \ No newline at end of file + --> + diff --git a/Application/FileConverter/Helpers.cs b/Application/FileConverter/Helpers.cs index a5f4207e..8e8c85de 100644 --- a/Application/FileConverter/Helpers.cs +++ b/Application/FileConverter/Helpers.cs @@ -1,141 +1,141 @@ -// License: http://www.gnu.org/licenses/gpl.html GPL version 3. - -namespace FileConverter -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.IO; - using System.Reflection; - using System.Threading; - - using FileConverter.ConversionJobs; - using FileConverter.Services; - - using SharpShell.Helpers; - - using Microsoft.Win32; - using CommunityToolkit.Mvvm.DependencyInjection; - - public static class Helpers - { - public static readonly string[] CompatibleInputExtensions = { - "3gp","3gpp","aac","aiff","ape","arw","avi","avif","bik","bmp","cda","cr2","dds","dng","doc","docx", - "exr","flac","flv","gif","heic","ico","jfif","jpg","jpeg","m4a","m4b","m4v","mkv","mov","mp3","mp4", - "mpg","mpeg","nef","odp","ods","odt","oga","ogg","ogv","opus","pdf","png","ppt","pptx","psd", - "raf", "rm","svg","tga","tif","tiff", "ts", "vob","wav","webm","webp","wma","wmv","xls","xlsx" - }; - - public static string GetExtensionCategory(string extension) - { - switch (extension) - { - case "aac": - case "aiff": - case "ape": - case "cda": - case "flac": - case "mp3": - case "m4a": - case "m4b": - case "oga": - case "ogg": - case "opus": - case "wav": - case "wma": - return InputCategoryNames.Audio; - - case "3gp": - case "3gpp": - case "avi": - case "bik": - case "flv": - case "m4v": - case "mp4": - case "mpg": - case "mpeg": - case "mov": - case "mkv": - case "ogv": - case "rm": - case "ts": - case "vob": - case "webm": - case "wmv": - return InputCategoryNames.Video; - - case "arw": - case "avif": - case "bmp": - case "cr2": - case "dds": - case "dng": - case "exr": - case "heic": - case "ico": - case "jfif": - case "jpg": - case "jpeg": - case "nef": - case "png": - case "psd": - case "raf": - case "tga": - case "tif": - case "tiff": - case "svg": - case "xcf": - case "webp": - return InputCategoryNames.Image; - - case "gif": - return InputCategoryNames.AnimatedImage; - - case "pdf": - case "doc": - case "docx": - case "ppt": - case "pptx": - case "odp": - case "ods": - case "odt": - case "xls": - case "xlsx": - return InputCategoryNames.Document; - } - - return InputCategoryNames.Misc; - } - +// License: http://www.gnu.org/licenses/gpl.html GPL version 3. + +namespace FileConverter +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Reflection; + using System.Threading; + + using FileConverter.ConversionJobs; + using FileConverter.Services; + + using SharpShell.Helpers; + + using Microsoft.Win32; + using CommunityToolkit.Mvvm.DependencyInjection; + + public static class Helpers + { + public static readonly string[] CompatibleInputExtensions = { + "3gp","3gpp","aac","aiff","ape","arw","avi","avif","bik","bmp","cda","cr2","dds","dng","doc","docx", + "exr","flac","flv","gif","heic","ico","jfif","jpg","jpeg","m4a","m4b","m4v","mkv","mov","mp3","mp4", + "mpg","mpeg","nef","odp","ods","odt","oga","ogg","ogv","opus","pdf","png","ppt","pptx","psd", + "raf", "rm","svg","tga","tif","tiff", "ts", "vob","wav","webm","webp","wma","wmv","xls","xlsx" + }; + + public static string GetExtensionCategory(string extension) + { + switch (extension) + { + case "aac": + case "aiff": + case "ape": + case "cda": + case "flac": + case "mp3": + case "m4a": + case "m4b": + case "oga": + case "ogg": + case "opus": + case "wav": + case "wma": + return InputCategoryNames.Audio; + + case "3gp": + case "3gpp": + case "avi": + case "bik": + case "flv": + case "m4v": + case "mp4": + case "mpg": + case "mpeg": + case "mov": + case "mkv": + case "ogv": + case "rm": + case "ts": + case "vob": + case "webm": + case "wmv": + return InputCategoryNames.Video; + + case "arw": + case "avif": + case "bmp": + case "cr2": + case "dds": + case "dng": + case "exr": + case "heic": + case "ico": + case "jfif": + case "jpg": + case "jpeg": + case "nef": + case "png": + case "psd": + case "raf": + case "tga": + case "tif": + case "tiff": + case "svg": + case "xcf": + case "webp": + return InputCategoryNames.Image; + + case "gif": + return InputCategoryNames.AnimatedImage; + + case "pdf": + case "doc": + case "docx": + case "ppt": + case "pptx": + case "odp": + case "ods": + case "odt": + case "xls": + case "xlsx": + return InputCategoryNames.Document; + } + + return InputCategoryNames.Misc; + } + public static bool RegisterShellExtension(string shellExtensionPath) { if (!Application.IsInAdmininstratorPrivileges) { - Diagnostics.Debug.LogError("File Converter needs administrator privileges to register the shell extension."); - return false; - } - - if (!File.Exists(shellExtensionPath)) - { - Diagnostics.Debug.LogError($"Shell extension {shellExtensionPath} does not exists."); - return false; - } - - Diagnostics.Debug.Log($"Install and register shell extension: {shellExtensionPath}."); - - var regasm = new RegAsm(); - var success = regasm.Register64(shellExtensionPath, true); - if (success) - { - Diagnostics.Debug.Log($"{shellExtensionPath} installed and registered."); - Diagnostics.Debug.Log(regasm.StandardOutput); - return true; - } - else - { - Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to register."); - Diagnostics.Debug.LogError(regasm.StandardError); - return false; + Diagnostics.Debug.LogError("ZFileConverter needs administrator privileges to register the shell extension."); + return false; + } + + if (!File.Exists(shellExtensionPath)) + { + Diagnostics.Debug.LogError($"Shell extension {shellExtensionPath} does not exists."); + return false; + } + + Diagnostics.Debug.Log($"Install and register shell extension: {shellExtensionPath}."); + + var regasm = new RegAsm(); + var success = regasm.Register64(shellExtensionPath, true); + if (success) + { + Diagnostics.Debug.Log($"{shellExtensionPath} installed and registered."); + Diagnostics.Debug.Log(regasm.StandardOutput); + return true; + } + else + { + Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to register."); + Diagnostics.Debug.LogError(regasm.StandardError); + return false; } } @@ -150,7 +150,7 @@ public static bool RepairShellExtension(string shellExtensionPath) { if (!Application.IsInAdmininstratorPrivileges) { - Diagnostics.Debug.LogError("File Converter needs administrator privileges to repair the shell extension."); + Diagnostics.Debug.LogError("ZFileConverter needs administrator privileges to repair the shell extension."); return false; } @@ -191,215 +191,215 @@ public static bool UnregisterExtension(string shellExtensionPath) { if (!Application.IsInAdmininstratorPrivileges) { - Diagnostics.Debug.LogError("File Converter needs administrator privileges to unregister the shell extension."); - return false; - } - - if (!File.Exists(shellExtensionPath)) - { - Diagnostics.Debug.LogError($"Shell extension {shellExtensionPath} does not exists."); - return false; - } - - Diagnostics.Debug.Log($"Unregister and uninstall shell extension: {shellExtensionPath}."); - - var regasm = new RegAsm(); - var success = regasm.Unregister64(shellExtensionPath); - if (success) - { - Diagnostics.Debug.Log($"{shellExtensionPath} uninstalled."); - Diagnostics.Debug.Log(regasm.StandardOutput); - return true; - } - else - { - Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to uninstall."); - Diagnostics.Debug.LogError(regasm.StandardError); - return false; - } - } - - public static IEnumerable GetSupportedCultures() - { - // Get all cultures. - CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures); - - // Find the location where application installed. - string exeLocation = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path)); - - // Return all culture for which satellite folder found with culture code. - foreach (CultureInfo cultureInfo in cultures) - { - if (!string.IsNullOrEmpty(cultureInfo.Name) && Directory.Exists(Path.Combine(exeLocation, "Languages", cultureInfo.Name))) - { - yield return cultureInfo; - } - } - } - - public static bool IsOutputTypeCompatibleWithCategory(OutputType outputType, string category) - { - if (category == InputCategoryNames.Misc) - { - // Misc category contains unsorted input extensions, so we consider that they are compatible to be tolerant. - return true; - } - - switch (outputType) - { - case OutputType.Aac: - case OutputType.Flac: - case OutputType.Mp3: - case OutputType.Ogg: - case OutputType.Wav: - return category == InputCategoryNames.Audio || category == InputCategoryNames.Video; - - case OutputType.Avi: - case OutputType.Mkv: - case OutputType.Mp4: - case OutputType.Ogv: - case OutputType.Webm: - return category == InputCategoryNames.Video || category == InputCategoryNames.AnimatedImage; - - case OutputType.Avif: - case OutputType.Ico: - case OutputType.Jpg: - case OutputType.Png: - case OutputType.Webp: - return category == InputCategoryNames.Image || category == InputCategoryNames.Document || category == InputCategoryNames.AnimatedImage; - - case OutputType.Gif: - return category == InputCategoryNames.Image || category == InputCategoryNames.Video || category == InputCategoryNames.AnimatedImage; - - case OutputType.Pdf: - return category == InputCategoryNames.Image || category == InputCategoryNames.Document; - - default: - return false; - } - } - - public static Thread InstantiateThread(string name, ThreadStart threadStart) - { - ISettingsService settingsService = Ioc.Default.GetRequiredService(); - CultureInfo currentCulture = settingsService?.Settings?.ApplicationLanguage; - - Thread thread = new Thread(threadStart); - thread.Name = name; - - if (currentCulture != null) - { - thread.CurrentCulture = currentCulture; - thread.CurrentUICulture = currentCulture; - } - - return thread; - } - - public static Thread InstantiateThread(string name, ParameterizedThreadStart parameterizedThreadStart) - { - ISettingsService settingsService = Ioc.Default.GetRequiredService(); - CultureInfo currentCulture = settingsService?.Settings?.ApplicationLanguage; - - Thread thread = new Thread(parameterizedThreadStart); - thread.Name = name; - - if (currentCulture != null) - { - thread.CurrentCulture = currentCulture; - thread.CurrentUICulture = currentCulture; - } - - return thread; - } - - /// - /// Check whether Microsoft office is available or not. - /// - /// The office application name. - /// Returns true if Office is installed on the computer. - /// source: http://stackoverflow.com/questions/3266675/how-to-detect-installed-version-of-ms-office/3267832#3267832 - /// source: http://www.codeproject.com/Articles/26520/Getting-Office-s-Version - public static bool IsMicrosoftOfficeApplicationAvailable(ConversionJobs.ConversionJob_Office.ApplicationName application) - { - string registryKeyPattern = @"Software\Microsoft\Windows\CurrentVersion\App Paths\"; - switch (application) - { - case ConversionJob_Office.ApplicationName.Word: - registryKeyPattern += "winword.exe"; - break; - - case ConversionJob_Office.ApplicationName.PowerPoint: - registryKeyPattern += "powerpnt.exe"; - break; - - case ConversionJob_Office.ApplicationName.Excel: - registryKeyPattern += "excel.exe"; - break; - - case ConversionJob_Office.ApplicationName.None: - return false; - } - - // Looks inside CURRENT_USER. - RegistryKey winwordKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(registryKeyPattern, false); - if (winwordKey != null) - { - string winwordPath = winwordKey.GetValue(string.Empty).ToString(); - if (!string.IsNullOrEmpty(winwordPath)) - { - return true; - } - } - - // If not found, looks inside LOCAL_MACHINE. - winwordKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(registryKeyPattern, false); - if (winwordKey != null) - { - string winwordPath = winwordKey.GetValue(string.Empty).ToString(); - if (!string.IsNullOrEmpty(winwordPath)) - { - return true; - } - } - - return false; - } - - public static ConversionJob_Office.ApplicationName GetOfficeApplicationCompatibleWithExtension(string extension) - { - switch (extension) - { - case "doc": - case "docx": - case "odt": - return ConversionJob_Office.ApplicationName.Word; - - case "ppt": - case "pptx": - case "odp": - return ConversionJob_Office.ApplicationName.PowerPoint; - - case "ods": - case "xls": - case "xlsx": - return ConversionJob_Office.ApplicationName.Excel; - } - - return ConversionJob_Office.ApplicationName.None; - } - - public static class InputCategoryNames - { - public const string Audio = "Audio"; - public const string Video = "Video"; - public const string Image = "Image"; - public const string AnimatedImage = "Animated Image"; - public const string Document = "Document"; - - public const string Misc = "Misc"; - } - + Diagnostics.Debug.LogError("ZFileConverter needs administrator privileges to unregister the shell extension."); + return false; + } + + if (!File.Exists(shellExtensionPath)) + { + Diagnostics.Debug.LogError($"Shell extension {shellExtensionPath} does not exists."); + return false; + } + + Diagnostics.Debug.Log($"Unregister and uninstall shell extension: {shellExtensionPath}."); + + var regasm = new RegAsm(); + var success = regasm.Unregister64(shellExtensionPath); + if (success) + { + Diagnostics.Debug.Log($"{shellExtensionPath} uninstalled."); + Diagnostics.Debug.Log(regasm.StandardOutput); + return true; + } + else + { + Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to uninstall."); + Diagnostics.Debug.LogError(regasm.StandardError); + return false; + } + } + + public static IEnumerable GetSupportedCultures() + { + // Get all cultures. + CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures); + + // Find the location where application installed. + string exeLocation = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path)); + + // Return all culture for which satellite folder found with culture code. + foreach (CultureInfo cultureInfo in cultures) + { + if (!string.IsNullOrEmpty(cultureInfo.Name) && Directory.Exists(Path.Combine(exeLocation, "Languages", cultureInfo.Name))) + { + yield return cultureInfo; + } + } + } + + public static bool IsOutputTypeCompatibleWithCategory(OutputType outputType, string category) + { + if (category == InputCategoryNames.Misc) + { + // Misc category contains unsorted input extensions, so we consider that they are compatible to be tolerant. + return true; + } + + switch (outputType) + { + case OutputType.Aac: + case OutputType.Flac: + case OutputType.Mp3: + case OutputType.Ogg: + case OutputType.Wav: + return category == InputCategoryNames.Audio || category == InputCategoryNames.Video; + + case OutputType.Avi: + case OutputType.Mkv: + case OutputType.Mp4: + case OutputType.Ogv: + case OutputType.Webm: + return category == InputCategoryNames.Video || category == InputCategoryNames.AnimatedImage; + + case OutputType.Avif: + case OutputType.Ico: + case OutputType.Jpg: + case OutputType.Png: + case OutputType.Webp: + return category == InputCategoryNames.Image || category == InputCategoryNames.Document || category == InputCategoryNames.AnimatedImage; + + case OutputType.Gif: + return category == InputCategoryNames.Image || category == InputCategoryNames.Video || category == InputCategoryNames.AnimatedImage; + + case OutputType.Pdf: + return category == InputCategoryNames.Image || category == InputCategoryNames.Document; + + default: + return false; + } + } + + public static Thread InstantiateThread(string name, ThreadStart threadStart) + { + ISettingsService settingsService = Ioc.Default.GetRequiredService(); + CultureInfo currentCulture = settingsService?.Settings?.ApplicationLanguage; + + Thread thread = new Thread(threadStart); + thread.Name = name; + + if (currentCulture != null) + { + thread.CurrentCulture = currentCulture; + thread.CurrentUICulture = currentCulture; + } + + return thread; + } + + public static Thread InstantiateThread(string name, ParameterizedThreadStart parameterizedThreadStart) + { + ISettingsService settingsService = Ioc.Default.GetRequiredService(); + CultureInfo currentCulture = settingsService?.Settings?.ApplicationLanguage; + + Thread thread = new Thread(parameterizedThreadStart); + thread.Name = name; + + if (currentCulture != null) + { + thread.CurrentCulture = currentCulture; + thread.CurrentUICulture = currentCulture; + } + + return thread; + } + + /// + /// Check whether Microsoft office is available or not. + /// + /// The office application name. + /// Returns true if Office is installed on the computer. + /// source: http://stackoverflow.com/questions/3266675/how-to-detect-installed-version-of-ms-office/3267832#3267832 + /// source: http://www.codeproject.com/Articles/26520/Getting-Office-s-Version + public static bool IsMicrosoftOfficeApplicationAvailable(ConversionJobs.ConversionJob_Office.ApplicationName application) + { + string registryKeyPattern = @"Software\Microsoft\Windows\CurrentVersion\App Paths\"; + switch (application) + { + case ConversionJob_Office.ApplicationName.Word: + registryKeyPattern += "winword.exe"; + break; + + case ConversionJob_Office.ApplicationName.PowerPoint: + registryKeyPattern += "powerpnt.exe"; + break; + + case ConversionJob_Office.ApplicationName.Excel: + registryKeyPattern += "excel.exe"; + break; + + case ConversionJob_Office.ApplicationName.None: + return false; + } + + // Looks inside CURRENT_USER. + RegistryKey winwordKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(registryKeyPattern, false); + if (winwordKey != null) + { + string winwordPath = winwordKey.GetValue(string.Empty).ToString(); + if (!string.IsNullOrEmpty(winwordPath)) + { + return true; + } + } + + // If not found, looks inside LOCAL_MACHINE. + winwordKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(registryKeyPattern, false); + if (winwordKey != null) + { + string winwordPath = winwordKey.GetValue(string.Empty).ToString(); + if (!string.IsNullOrEmpty(winwordPath)) + { + return true; + } + } + + return false; + } + + public static ConversionJob_Office.ApplicationName GetOfficeApplicationCompatibleWithExtension(string extension) + { + switch (extension) + { + case "doc": + case "docx": + case "odt": + return ConversionJob_Office.ApplicationName.Word; + + case "ppt": + case "pptx": + case "odp": + return ConversionJob_Office.ApplicationName.PowerPoint; + + case "ods": + case "xls": + case "xlsx": + return ConversionJob_Office.ApplicationName.Excel; + } + + return ConversionJob_Office.ApplicationName.None; + } + + public static class InputCategoryNames + { + public const string Audio = "Audio"; + public const string Video = "Video"; + public const string Image = "Image"; + public const string AnimatedImage = "Animated Image"; + public const string Document = "Document"; + + public const string Misc = "Misc"; + } + public enum HardwareAccelerationMode { Off, diff --git a/Application/FileConverter/Properties/AssemblyInfo.cs b/Application/FileConverter/Properties/AssemblyInfo.cs index fb176737..5a514370 100644 --- a/Application/FileConverter/Properties/AssemblyInfo.cs +++ b/Application/FileConverter/Properties/AssemblyInfo.cs @@ -5,12 +5,12 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("FileConverter")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyTitle("ZFileConverter")] +[assembly: AssemblyDescription("A maintained, Explorer-first file conversion utility.")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("FileConverter")] -[assembly: AssemblyCopyright("Copyright © 2026")] +[assembly: AssemblyCompany("ZaidNAlAsali")] +[assembly: AssemblyProduct("ZFileConverter")] +[assembly: AssemblyCopyright("Copyright © 2026 ZaidNAlAsali and File Converter contributors")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Application/FileConverter/Properties/Resources.en.resx b/Application/FileConverter/Properties/Resources.en.resx index 4957eba5..8270ad3d 100644 --- a/Application/FileConverter/Properties/Resources.en.resx +++ b/Application/FileConverter/Properties/Resources.en.resx @@ -136,7 +136,7 @@ Audio - Automatically check for updates when File Converter starts + Automatically check for updates when ZFileConverter starts Automatically exit when all conversions are complete @@ -172,10 +172,10 @@ Don't forget to reward my work if you like it :) - The update will be downloaded in background, and installed once you exit File Converter. + The update will be downloaded in background, and installed once you exit ZFileConverter. - Install when I exit File Converter + Install when I exit ZFileConverter Encoding : @@ -184,7 +184,7 @@ Encoding speed : - File Converter is a shell extension. + ZFileConverter is a shell extension. That means it is integrated into the Windows explorer. @@ -217,10 +217,10 @@ None - The update has been downloaded. Exit File Converter and install the update now. + The update has been downloaded. Exit ZFileConverter and install the update now. - Install when I exit File Converter + Install when I exit ZFileConverter Report an issue @@ -315,7 +315,7 @@ Use uppercase tokens for uppercase values when available. See change log ... - File Converter Settings + ZFileConverter Settings Settings @@ -327,13 +327,13 @@ Use uppercase tokens for uppercase values when available. 90° clockwise rotation - An update for File Converter is available. It is strongly recommended that you install it as soon as possible! + An update for ZFileConverter is available. It is strongly recommended that you install it as soon as possible! Upgrade download in progress... - File Converter Updates + ZFileConverter Updates Defines how 'file size' relates to 'video quality'. A lower value will give you a smaller file, a greater value give you higher video quality. @@ -501,7 +501,7 @@ Use uppercase tokens for uppercase values when available. Stereo - To use File Converter please open the explorer and right-click on any file you like to bring up the context menu where you will find File Converter commands (see the online documentation to see the list of compatible file formats). + To use ZFileConverter, open Explorer and right-click any supported file to bring up the context menu where you will find ZFileConverter commands (see the online documentation for compatible file formats). **This program is free software**. @@ -549,7 +549,7 @@ Use uppercase tokens for uppercase values when available. Microsoft Word must be installed in order to convert Word documents. - Open File Converter website + Open ZFileConverter releases Error during conversion job initialization. diff --git a/Application/FileConverter/Properties/Resources.resx b/Application/FileConverter/Properties/Resources.resx index b5384f17..c2b0e1ef 100644 --- a/Application/FileConverter/Properties/Resources.resx +++ b/Application/FileConverter/Properties/Resources.resx @@ -1,274 +1,273 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - About - - - Allow you to choose what you want to do with your input files if the conversion succeed - - - Action when conversion succeed - - - New preset - - - Application - - - Audio - - - Automatically check for updates when File Converter starts - - - Automatically exit when all conversions finished - - - Clamp to lowest power of 2 size - - - Close - - - Conversion Presets - - - Files to convert - - - Converted from - - - Logs - - - Logs - - - Open documentation - - - Donate - - - Don't forget to reward my work if you like it :) - - - The update will be downloaded in background, and installed once you exit File Converter. - - - Install when I exit File Converter - - - Encoding : - - - Encoding speed : - - - File Converter is a shell extension. - - - That means it is integrated into the Windows explorer. - - - To use File Converter please open the explorer and right-click on any file you like to bring up the context menu where you will find File Converter commands (see the readme file to see the list of compatible file formats). - - - File name template - - - Frames per second : - - - Open GitHub project page - - - help ? - - - Input example - - - Input formats - - - Delete - - - Move in an archive folder - - - None - - - The update has been downloaded. Exit File Converter and install the update now. - - - Install when I exit File Converter - - - Report an issue - - - Move down selected preset - - - Move up selected preset - - - Constant bitrate encoding - - - Variable bitrate encoding - - - 90° - - - 90° counter clockwise rotation - - - Ok - - - 180° - - - 180° rotation - - - C:\Music\Artist\Album\Song.wav - - - Output - - - (p): input file path -(f): input filename -(o): output extension type -(i): input extension type -(d0): input parent folder -(d1): input sub parent folder -... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + About + + + Allow you to choose what you want to do with your input files if the conversion succeed + + + Action when conversion succeed + + + New preset + + + Application + + + Audio + + + Automatically check for updates when ZFileConverter starts + + + Automatically exit when all conversions finished + + + Clamp to lowest power of 2 size + + + Close + + + Conversion Presets + + + Files to convert + + + Converted from + + + Logs + + + Logs + + + Open documentation + + + Donate + + + Don't forget to reward my work if you like it :) + + + The update will be downloaded in background, and installed once you exit ZFileConverter. + + + Install when I exit ZFileConverter + + + Encoding : + + + Encoding speed : + + + ZFileConverter is a shell extension. + + + That means it is integrated into the Windows explorer. + + + To use ZFileConverter, open Explorer and right-click any supported file to bring up the context menu where you will find ZFileConverter commands (see the readme file for compatible file formats). + + + File name template + + + Frames per second : + + + Open GitHub project page + + + help ? + + + Input example + + + Input formats + + + Delete + + + Move in an archive folder + + + None + + + The update has been downloaded. Exit ZFileConverter and install the update now. + + + Install when I exit ZFileConverter + + + Report an issue + + + Move down selected preset + + + Move up selected preset + + + Constant bitrate encoding + + + Variable bitrate encoding + + + 90° + + + 90° counter clockwise rotation + + + Ok + + + 180° + + + 180° rotation + + + C:\Music\Artist\Album\Song.wav + + + Output + + + (p): input file path +(f): input filename +(o): output extension type +(i): input extension type +(d0): input parent folder +(d1): input sub parent folder +... (n:i): page number index (n:c): total page count (n:i:D3): formatted page number index @@ -287,340 +286,340 @@ Special paths: Use uppercase tokens for uppercase values when available. - - Output format - - - Preset - - - Preset Name - - - Quality : - - - Recommended bitrate range in blue - - - Delete - - - Rotate : - - - Save - - - Scale : - - - See change log ... - - - Settings - - - Settings - - - 270° - - - 90° clockwise rotation - - - An update for File Converter is available. It is strongly recommended that you install it as soon as possible! - - - Upgrade download in progress... - - - File Converter Updates - - - Define the ratio 'file size' versus 'video quality'. A lower value will give you a smaller file, a greater value give you a better video quality. - - - Faster - - - Fast - - - Medium - - - Slower - - - Slow - - - Super Fast - - - Define the ratio 'file size' versus 'compression duration'. A slow compression will give you a smaller file (for the same video quality) than a faster compression. - - - Ultra Fast - - - Very Fast - - - Very Slow - - - Video - - - PCM signed 16-bit little-endian - - - PCM signed 24-bit little-endian - - - PCM signed 32-bit little-endian - - - PCM signed 8-bit little-endian - - - None - - - No rotation - - - Conversion - - - Done - - - Extraction - - - Failed - - - In queue - - - Read input image - - - Read document - - - Animated Image - - - Audio - - - Document - - - Image - - - Video - - - Waiting duration before application exit - - - Language - - - Maximum number of simultaneous conversions - - - New preset - - - ###Downloading change log ... - - - Conversion Archives - - - Canceled. - - - Fail to find ffmpeg executable. You should try to reinstall the application. - - - Audio CD track extraction failed. - - - CD drive is not ready. - - - Fail to launch ffmpeg. - - - Fail to create output path folders. - - - Fail to generate a unique output file path. - - - Fail to read cd drive '{0}'. - - - Fail to retrieve input path drive letter. - - - Fail to retrieve track number from the cda input path. - - - Fail to use the CD drive because it is opened. - - - The input file type is not compatible with the selected output file type. - - - Invalid output path generated by output file path template. - - - Unsupported output format '{0}'. - - - Channel count : - - - Change the number of channels of the input file. - - - Mono - - - Same than input file - - - Stereo - - - **This program is free software**. - - - You can redistribute it and/or modify it under the terms of the GNU General Public License. - - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. See the GNU General Public License (available in the installation folder: `LICENSE.md`) for more details. - - - The application is terminating. - - - The application will automatically terminate in {0} seconds. - - - The application will automatically terminate in 1 second. - - - Misc - - - Can't find the output file(s). - - - The conversion job failed but there is an output file that does exists. - - - Prepare conversion - - - Microsoft Office must be installed in order to convert Office documents. - - - Fail to open document with Microsoft Office (check if your licence/installation is valid). - - - Microsoft Excel must be installed in order to convert Excel documents. - - - Microsoft PowerPoint must be installed in order to convert PowerPoint documents. - - - Microsoft Word must be installed in order to convert Word documents. - - - Open File Converter website - - - Error during conversion job initialization. - - - New folder - - - New folder - - - Folder - - - Folder Name - - - Advanced mode - - - Custom Arguments - - - Custom FFMPEG command line arguments - - - No preset selected - - - Export - - - Import - - - Duplicate preset - - - Cancel - - - Error - - - Can't load file converter user settings. Do you want to fall back to default settings ? - - - Copy files in clipboard after conversion - - - Hardware acceleration mode - - - Advanced Media Framework (AMD) - - - CUDA (Nvidia) - - - Direct3D 11 - - - Direct3D 9/DXVA2 - - - Off - - - OpenCL - - - Vulkan - + + Output format + + + Preset + + + Preset Name + + + Quality : + + + Recommended bitrate range in blue + + + Delete + + + Rotate : + + + Save + + + Scale : + + + See change log ... + + + Settings + + + Settings + + + 270° + + + 90° clockwise rotation + + + An update for ZFileConverter is available. It is strongly recommended that you install it as soon as possible! + + + Upgrade download in progress... + + + ZFileConverter Updates + + + Define the ratio 'file size' versus 'video quality'. A lower value will give you a smaller file, a greater value give you a better video quality. + + + Faster + + + Fast + + + Medium + + + Slower + + + Slow + + + Super Fast + + + Define the ratio 'file size' versus 'compression duration'. A slow compression will give you a smaller file (for the same video quality) than a faster compression. + + + Ultra Fast + + + Very Fast + + + Very Slow + + + Video + + + PCM signed 16-bit little-endian + + + PCM signed 24-bit little-endian + + + PCM signed 32-bit little-endian + + + PCM signed 8-bit little-endian + + + None + + + No rotation + + + Conversion + + + Done + + + Extraction + + + Failed + + + In queue + + + Read input image + + + Read document + + + Animated Image + + + Audio + + + Document + + + Image + + + Video + + + Waiting duration before application exit + + + Language + + + Maximum number of simultaneous conversions + + + New preset + + + ###Downloading change log ... + + + Conversion Archives + + + Canceled. + + + Fail to find ffmpeg executable. You should try to reinstall the application. + + + Audio CD track extraction failed. + + + CD drive is not ready. + + + Fail to launch ffmpeg. + + + Fail to create output path folders. + + + Fail to generate a unique output file path. + + + Fail to read cd drive '{0}'. + + + Fail to retrieve input path drive letter. + + + Fail to retrieve track number from the cda input path. + + + Fail to use the CD drive because it is opened. + + + The input file type is not compatible with the selected output file type. + + + Invalid output path generated by output file path template. + + + Unsupported output format '{0}'. + + + Channel count : + + + Change the number of channels of the input file. + + + Mono + + + Same than input file + + + Stereo + + + **This program is free software**. + + + You can redistribute it and/or modify it under the terms of the GNU General Public License. + + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. See the GNU General Public License (available in the installation folder: `LICENSE.md`) for more details. + + + The application is terminating. + + + The application will automatically terminate in {0} seconds. + + + The application will automatically terminate in 1 second. + + + Misc + + + Can't find the output file(s). + + + The conversion job failed but there is an output file that does exists. + + + Prepare conversion + + + Microsoft Office must be installed in order to convert Office documents. + + + Fail to open document with Microsoft Office (check if your licence/installation is valid). + + + Microsoft Excel must be installed in order to convert Excel documents. + + + Microsoft PowerPoint must be installed in order to convert PowerPoint documents. + + + Microsoft Word must be installed in order to convert Word documents. + + + Open ZFileConverter releases + + + Error during conversion job initialization. + + + New folder + + + New folder + + + Folder + + + Folder Name + + + Advanced mode + + + Custom Arguments + + + Custom FFMPEG command line arguments + + + No preset selected + + + Export + + + Import + + + Duplicate preset + + + Cancel + + + Error + + + Can't load file converter user settings. Do you want to fall back to default settings ? + + + Copy files in clipboard after conversion + + + Hardware acceleration mode + + + Advanced Media Framework (AMD) + + + CUDA (Nvidia) + + + Direct3D 11 + + + Direct3D 9/DXVA2 + + + Off + + + OpenCL + + + Vulkan + diff --git a/Application/FileConverter/Services/ConversionJobRegisteredEventArgs.cs b/Application/FileConverter/Services/ConversionJobRegisteredEventArgs.cs new file mode 100644 index 00000000..f80f1192 --- /dev/null +++ b/Application/FileConverter/Services/ConversionJobRegisteredEventArgs.cs @@ -0,0 +1,21 @@ +// License: http://www.gnu.org/licenses/gpl.html GPL version 3. + +namespace FileConverter.Services +{ + using System; + + using FileConverter.ConversionJobs; + + public class ConversionJobRegisteredEventArgs : EventArgs + { + public ConversionJobRegisteredEventArgs(ConversionJob conversionJob) + { + this.ConversionJob = conversionJob; + } + + public ConversionJob ConversionJob + { + get; + } + } +} diff --git a/Application/FileConverter/Services/ConversionService.cs b/Application/FileConverter/Services/ConversionService.cs index 99141a8e..934c5b57 100644 --- a/Application/FileConverter/Services/ConversionService.cs +++ b/Application/FileConverter/Services/ConversionService.cs @@ -15,11 +15,13 @@ namespace FileConverter.Services public class ConversionService : ObservableObject, IConversionService { + private readonly object conversionQueueLock = new object(); private readonly List conversionJobs = new List(); private readonly int numberOfConversionThread = 1; private ISettingsService settingsService; + private bool conversionQueueIsRunning; public ConversionService(ISettingsService settingsService) { @@ -42,6 +44,8 @@ public ConversionService(ISettingsService settingsService) } } + public event System.EventHandler ConversionJobRegistered; + public event System.EventHandler ConversionJobsTerminated; public ReadOnlyCollection ConversionJobs @@ -52,107 +56,167 @@ public ReadOnlyCollection ConversionJobs public void RegisterConversionJob(ConversionJob conversionJob) { + if (conversionJob == null) + { + throw new ArgumentNullException(nameof(conversionJob)); + } + this.conversionJobs.Add(conversionJob); this.OnPropertyChanged(nameof(this.ConversionJobs)); + this.ConversionJobRegistered?.Invoke(this, new ConversionJobRegisteredEventArgs(conversionJob)); } public void ConvertFilesAsync() { + lock (this.conversionQueueLock) + { + if (this.conversionQueueIsRunning) + { + Debug.Log("Conversion queue is already running."); + return; + } + + this.conversionQueueIsRunning = true; + } + Thread fileConvertionThread = Helpers.InstantiateThread("ConversionQueueThread", this.ConvertFiles); fileConvertionThread.Start(); } - private void ConvertFiles() + public void RetryConversionJob(ConversionJob conversionJob) { - // Prepare conversions. - for (int index = 0; index < this.ConversionJobs.Count; index++) + if (conversionJob == null) { - this.ConversionJobs[index].PrepareConversion(); + throw new ArgumentNullException(nameof(conversionJob)); } - System.Collections.Specialized.StringCollection files = new System.Collections.Specialized.StringCollection(); - // Convert! - Thread[] jobThreads = new Thread[this.numberOfConversionThread]; - while (true) + lock (this.conversionQueueLock) { - // Compute conversion flags. - ConversionFlags conversionFlags = ConversionFlags.None; - bool allJobAreFinished = true; - for (int jobIndex = 0; jobIndex < this.conversionJobs.Count; jobIndex++) + if (this.conversionQueueIsRunning) { - ConversionJob conversionJob = this.conversionJobs[jobIndex]; - allJobAreFinished &= !(conversionJob.State == ConversionState.Ready || conversionJob.State == ConversionState.InProgress); + Debug.Log("Can't retry a conversion while the queue is running."); + return; + } + } + + ConversionJob retryJob = ConversionJobFactory.Create(conversionJob.ConversionPreset, conversionJob.InitialInputPath); + this.RegisterConversionJob(retryJob); + this.ConvertFilesAsync(); + } + + private void ConvertFiles() + { + try + { + List activeJobs = new List(); - if (conversionJob.State == ConversionState.InProgress) + // Prepare conversions. + for (int index = 0; index < this.ConversionJobs.Count; index++) + { + if (this.ConversionJobs[index].State == ConversionState.Unknown) { - conversionFlags |= conversionJob.StateFlags; + this.ConversionJobs[index].PrepareConversion(); + activeJobs.Add(this.ConversionJobs[index]); } } - if (allJobAreFinished) + if (activeJobs.Count == 0) { - break; + Debug.Log("No pending conversion jobs to run."); + return; } - // Start job if possible. - for (int jobIndex = 0; jobIndex < this.conversionJobs.Count; jobIndex++) + System.Collections.Specialized.StringCollection files = new System.Collections.Specialized.StringCollection(); + // Convert! + Thread[] jobThreads = new Thread[this.numberOfConversionThread]; + while (true) { - ConversionJob conversionJob = this.conversionJobs[jobIndex]; - if (conversionJob.State == ConversionState.Ready && conversionJob.CanStartConversion(conversionFlags)) + // Compute conversion flags. + ConversionFlags conversionFlags = ConversionFlags.None; + bool allJobAreFinished = true; + for (int jobIndex = 0; jobIndex < activeJobs.Count; jobIndex++) { - // Find a thread to execute the job. - Thread jobThread = null; - for (int threadIndex = 0; threadIndex < jobThreads.Length; threadIndex++) + ConversionJob conversionJob = activeJobs[jobIndex]; + allJobAreFinished &= !(conversionJob.State == ConversionState.Ready || conversionJob.State == ConversionState.InProgress); + + if (conversionJob.State == ConversionState.InProgress) { - Thread thread = jobThreads[threadIndex]; - if (thread == null || !thread.IsAlive) - { - jobThread = Helpers.InstantiateThread(conversionJob.GetType().Name, this.ExecuteConversionJob); - jobThreads[threadIndex] = jobThread; - break; - } + conversionFlags |= conversionJob.StateFlags; } + } + + if (allJobAreFinished) + { + break; + } - if (jobThread != null) + // Start job if possible. + for (int jobIndex = 0; jobIndex < activeJobs.Count; jobIndex++) + { + ConversionJob conversionJob = activeJobs[jobIndex]; + if (conversionJob.State == ConversionState.Ready && conversionJob.CanStartConversion(conversionFlags)) { - jobThread.Start(conversionJob); + // Find a thread to execute the job. + Thread jobThread = null; + for (int threadIndex = 0; threadIndex < jobThreads.Length; threadIndex++) + { + Thread thread = jobThreads[threadIndex]; + if (thread == null || !thread.IsAlive) + { + jobThread = Helpers.InstantiateThread(conversionJob.GetType().Name, this.ExecuteConversionJob); + jobThreads[threadIndex] = jobThread; + break; + } + } + + if (jobThread != null) + { + jobThread.Start(conversionJob); + + while (conversionJob.State == ConversionState.Ready) + { + Debug.Log("Wait the launch of the conversion thread before launching any other thread."); + Thread.Sleep(20); + } + } - while (conversionJob.State == ConversionState.Ready) + if (!files.Contains(conversionJob.OutputFilePath)) { - Debug.Log("Wait the launch of the conversion thread before launching any other thread."); - Thread.Sleep(20); + files.Add(conversionJob.OutputFilePath); } - } - if (!files.Contains(conversionJob.OutputFilePath)) - { - files.Add(conversionJob.OutputFilePath); + break; } - - break; } + + Thread.Sleep(50); } - Thread.Sleep(50); - } + // Copy the output files to the clipboard + if (this.settingsService.Settings.CopyFilesInClipboardAfterConversion && files.Count > 0) + { + Thread clipboardThread = Helpers.InstantiateThread("CopyFilesToClipboardThread", this.CopyFilesToClipboard); + clipboardThread.SetApartmentState(ApartmentState.STA); + clipboardThread.Start(files); + } - // Copy the output files to the clipboard - if (this.settingsService.Settings.CopyFilesInClipboardAfterConversion && files.Count > 0) - { - Thread clipboardThread = Helpers.InstantiateThread("CopyFilesToClipboardThread", this.CopyFilesToClipboard); - clipboardThread.SetApartmentState(ApartmentState.STA); - clipboardThread.Start(files); - } + bool allConversionsSucceed = true; + for (int index = 0; index < activeJobs.Count; index++) + { + allConversionsSucceed &= activeJobs[index].State == ConversionState.Done; + } - bool allConversionsSucceed = true; - for (int index = 0; index < this.conversionJobs.Count; index++) - { - allConversionsSucceed &= this.conversionJobs[index].State == ConversionState.Done; + if (this.ConversionJobsTerminated != null) + { + this.ConversionJobsTerminated.Invoke(this, new ConversionJobsTerminatedEventArgs(allConversionsSucceed)); + } } - - if (this.ConversionJobsTerminated != null) + finally { - this.ConversionJobsTerminated.Invoke(this, new ConversionJobsTerminatedEventArgs(allConversionsSucceed)); + lock (this.conversionQueueLock) + { + this.conversionQueueIsRunning = false; + } } } diff --git a/Application/FileConverter/Services/IConversionService.cs b/Application/FileConverter/Services/IConversionService.cs index e5216b09..7f201446 100644 --- a/Application/FileConverter/Services/IConversionService.cs +++ b/Application/FileConverter/Services/IConversionService.cs @@ -8,6 +8,8 @@ namespace FileConverter.Services public interface IConversionService { + event System.EventHandler ConversionJobRegistered; + event System.EventHandler ConversionJobsTerminated; ReadOnlyCollection ConversionJobs @@ -18,5 +20,7 @@ ReadOnlyCollection ConversionJobs void ConvertFilesAsync(); void RegisterConversionJob(ConversionJob conversionJob); + + void RetryConversionJob(ConversionJob conversionJob); } } diff --git a/Application/FileConverter/Services/UpgradeService.cs b/Application/FileConverter/Services/UpgradeService.cs index f0fb8600..6eea7aa5 100644 --- a/Application/FileConverter/Services/UpgradeService.cs +++ b/Application/FileConverter/Services/UpgradeService.cs @@ -18,9 +18,9 @@ namespace FileConverter.Services public class UpgradeService : ObservableObject, IUpgradeService { #if DEBUG - private const string BaseURI = "https://raw.githubusercontent.com/Tichau/FileConverter/integration/"; + private const string BaseURI = "https://raw.githubusercontent.com/ZaidNAlAsali/FileConverter/integration/"; #else - private const string BaseURI = "https://raw.githubusercontent.com/Tichau/FileConverter/master/"; + private const string BaseURI = "https://raw.githubusercontent.com/ZaidNAlAsali/FileConverter/master/"; #endif [NotNull] @@ -211,7 +211,7 @@ private async Task DownloadInstaller() Uri uri = new Uri(this.UpgradeVersionDescription.InstallerURL); - string fileName = "FileConverter-setup.msi"; + string fileName = "ZFileConverter-setup.msi"; Regex retrieveFileNameRegex = new Regex("/([^/]*)"); MatchCollection matchCollection = retrieveFileNameRegex.Matches(this.UpgradeVersionDescription.InstallerURL); if (matchCollection.Count > 0) @@ -244,7 +244,7 @@ private async Task DownloadInstaller() } catch (Exception exception) { - Debug.LogError("Failed to download the new File Converter upgrade. You should try again or download it manually."); + Debug.LogError("Failed to download the new ZFileConverter upgrade. You should try again or download it manually."); Debug.Log(exception.ToString()); this.UpgradeVersionDescription.InstallerDownloadInProgress = false; this.UpgradeVersionDescription.InstallerDownloadProgress = 0; diff --git a/Application/FileConverter/ValueConverters/ApplicationVersionToApplicationName.cs b/Application/FileConverter/ValueConverters/ApplicationVersionToApplicationName.cs index cc93d8c0..71d709e5 100644 --- a/Application/FileConverter/ValueConverters/ApplicationVersionToApplicationName.cs +++ b/Application/FileConverter/ValueConverters/ApplicationVersionToApplicationName.cs @@ -12,12 +12,12 @@ public object Convert(object value, Type targetType, object parameter, CultureIn { if (!(value is FileConverter.Version)) { - return "File Converter"; + return "ZFileConverter"; } FileConverter.Version version = (FileConverter.Version)value; - return $"File Converter v{version}"; + return $"ZFileConverter v{version}"; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/Application/FileConverter/ViewModels/DependencyStatusViewModel.cs b/Application/FileConverter/ViewModels/DependencyStatusViewModel.cs new file mode 100644 index 00000000..122e8f18 --- /dev/null +++ b/Application/FileConverter/ViewModels/DependencyStatusViewModel.cs @@ -0,0 +1,46 @@ +// License: http://www.gnu.org/licenses/gpl.html GPL version 3. + +namespace FileConverter.ViewModels +{ + using CommunityToolkit.Mvvm.ComponentModel; + + public class DependencyStatusViewModel : ObservableObject + { + private string name; + private string status; + private string details; + private bool isHealthy; + + public DependencyStatusViewModel(string name, string status, string details, bool isHealthy) + { + this.Name = name; + this.Status = status; + this.Details = details; + this.IsHealthy = isHealthy; + } + + public string Name + { + get => this.name; + private set => this.SetProperty(ref this.name, value); + } + + public string Status + { + get => this.status; + private set => this.SetProperty(ref this.status, value); + } + + public string Details + { + get => this.details; + private set => this.SetProperty(ref this.details, value); + } + + public bool IsHealthy + { + get => this.isHealthy; + private set => this.SetProperty(ref this.isHealthy, value); + } + } +} diff --git a/Application/FileConverter/ViewModels/DiagnosticsViewModel.cs b/Application/FileConverter/ViewModels/DiagnosticsViewModel.cs index c6552a00..e1022f38 100644 --- a/Application/FileConverter/ViewModels/DiagnosticsViewModel.cs +++ b/Application/FileConverter/ViewModels/DiagnosticsViewModel.cs @@ -3,6 +3,9 @@ namespace FileConverter.ViewModels { using System.ComponentModel; + using System.Diagnostics; + using System.IO; + using System.Windows; using System.Windows.Input; using CommunityToolkit.Mvvm.ComponentModel; @@ -16,7 +19,9 @@ namespace FileConverter.ViewModels /// public class DiagnosticsViewModel : ObservableRecipient { + private RelayCommand copyDiagnosticsCommand; private RelayCommand closeCommand; + private RelayCommand openDiagnosticsFolderCommand; /// /// Initializes a new instance of the DiagnosticsViewModel class. @@ -38,10 +43,67 @@ public ICommand CloseCommand } } + public ICommand CopyDiagnosticsCommand + { + get + { + if (this.copyDiagnosticsCommand == null) + { + this.copyDiagnosticsCommand = new RelayCommand(this.CopyDiagnostics); + } + + return this.copyDiagnosticsCommand; + } + } + + public ICommand OpenDiagnosticsFolderCommand + { + get + { + if (this.openDiagnosticsFolderCommand == null) + { + this.openDiagnosticsFolderCommand = new RelayCommand(this.OpenDiagnosticsFolder); + } + + return this.openDiagnosticsFolderCommand; + } + } + private void Close(CancelEventArgs args) { INavigationService navigationService = Ioc.Default.GetRequiredService(); navigationService.Close(Pages.Diagnostics, args != null); } + + private void CopyDiagnostics() + { + try + { + Clipboard.SetText(Diagnostics.Debug.AllContent); + } + catch (System.Exception exception) + { + Diagnostics.Debug.Log($"Can't copy diagnostics to clipboard: {exception.Message}."); + } + } + + private void OpenDiagnosticsFolder() + { + string diagnosticsFolderPath = Diagnostics.Debug.DiagnosticsFolderPath; + if (string.IsNullOrEmpty(diagnosticsFolderPath) || !Directory.Exists(diagnosticsFolderPath)) + { + Diagnostics.Debug.Log($"Can't open diagnostics folder: {diagnosticsFolderPath}."); + return; + } + + try + { + Process.Start("explorer.exe", $"\"{diagnosticsFolderPath}\""); + } + catch (System.Exception exception) + { + Diagnostics.Debug.Log($"Can't open diagnostics folder: {exception.Message}."); + } + } } } diff --git a/Application/FileConverter/ViewModels/MainViewModel.cs b/Application/FileConverter/ViewModels/MainViewModel.cs index e50a632c..948a0580 100644 --- a/Application/FileConverter/ViewModels/MainViewModel.cs +++ b/Application/FileConverter/ViewModels/MainViewModel.cs @@ -18,6 +18,8 @@ namespace FileConverter.ViewModels /// public class MainViewModel : ObservableRecipient { + private readonly IConversionService conversionService; + private string informationMessage; private ObservableCollection conversionJobs; @@ -30,8 +32,9 @@ public class MainViewModel : ObservableRecipient /// public MainViewModel() { - IConversionService settingsService = Ioc.Default.GetRequiredService(); - this.ConversionJobs = new ObservableCollection(settingsService.ConversionJobs); + this.conversionService = Ioc.Default.GetRequiredService(); + this.conversionService.ConversionJobRegistered += this.ConversionService_ConversionJobRegistered; + this.ConversionJobs = new ObservableCollection(this.conversionService.ConversionJobs); Application application = Application.Current as Application; application.OnApplicationTerminate += this.Application_OnApplicationTerminate; @@ -117,6 +120,21 @@ private void ConversionJob_PropertyChanged(object sender, PropertyChangedEventAr this.OnPropertyChanged(nameof(this.ConversionJobs)); } + private void ConversionService_ConversionJobRegistered(object sender, ConversionJobRegisteredEventArgs eventArgs) + { + if (eventArgs?.ConversionJob == null || this.ConversionJobs.Contains(eventArgs.ConversionJob)) + { + return; + } + + Application.Current.Dispatcher.Invoke(() => + { + this.ConversionJobs.Add(eventArgs.ConversionJob); + eventArgs.ConversionJob.PropertyChanged += this.ConversionJob_PropertyChanged; + this.OnPropertyChanged(nameof(this.ConversionJobs)); + }); + } + private void Application_OnApplicationTerminate(object sender, ApplicationTerminateArgs eventArgs) { if (float.IsNaN(eventArgs.RemainingTimeBeforeTermination)) @@ -142,4 +160,4 @@ private void Application_OnApplicationTerminate(object sender, ApplicationTermin } } } -} \ No newline at end of file +} diff --git a/Application/FileConverter/ViewModels/SettingsViewModel.cs b/Application/FileConverter/ViewModels/SettingsViewModel.cs index fd610ac9..6c017bfb 100644 --- a/Application/FileConverter/ViewModels/SettingsViewModel.cs +++ b/Application/FileConverter/ViewModels/SettingsViewModel.cs @@ -5,10 +5,12 @@ namespace FileConverter.ViewModels using System; using System.IO; using System.Collections.Generic; + using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.Linq; + using System.Reflection; using System.Windows.Data; using System.Windows.Input; @@ -19,6 +21,7 @@ namespace FileConverter.ViewModels using CommunityToolkit.Mvvm.Input; using FileConverter.Annotations; + using FileConverter.ConversionJobs; using FileConverter.Services; using FileConverter.Views; @@ -42,9 +45,14 @@ public class SettingsViewModel : ObservableRecipient, IDataErrorInfo private RelayCommand importPresetCommand; private RelayCommand exportPresetCommand; private RelayCommand removePresetCommand; + private RelayCommand refreshDependencyHealthCommand; + private RelayCommand repairShellExtensionCommand; private RelayCommand saveCommand; private RelayCommand closeCommand; + private ObservableCollection dependencyStatuses = new ObservableCollection(); + private string shellExtensionRepairStatus = string.Empty; + private ListCollectionView outputTypes; private CultureInfo[] supportedCultures; private Helpers.HardwareAccelerationMode[] hardwareAccelerationModes = { Helpers.HardwareAccelerationMode.Off, Helpers.HardwareAccelerationMode.CUDA, Helpers.HardwareAccelerationMode.AMF }; @@ -65,6 +73,8 @@ public SettingsViewModel() this.importPresetCommand = new RelayCommand(this.ImportPreset); this.exportPresetCommand = new RelayCommand(this.ExportSelectedPreset, this.CanExportSelectedPreset); this.removePresetCommand = new RelayCommand(this.RemoveSelectedPreset, this.CanRemoveSelectedPreset); + this.refreshDependencyHealthCommand = new RelayCommand(this.RefreshDependencyHealth); + this.repairShellExtensionCommand = new RelayCommand(this.RepairShellExtension); this.saveCommand = new RelayCommand(this.SaveSettings, this.CanSaveSettings); this.closeCommand = new RelayCommand(this.CloseSettings); @@ -96,6 +106,7 @@ public SettingsViewModel() this.InitializeCompatibleInputExtensions(); this.InitializePresetFolders(); + this.RefreshDependencyHealth(); } public IEnumerable InputCategories @@ -234,14 +245,14 @@ public CultureInfo[] SupportedCultures } } - public Helpers.HardwareAccelerationMode[] HardwareAccelerationModes - { - get => this.hardwareAccelerationModes; - set - { - this.hardwareAccelerationModes = value; - this.OnPropertyChanged(); - } + public Helpers.HardwareAccelerationMode[] HardwareAccelerationModes + { + get => this.hardwareAccelerationModes; + set + { + this.hardwareAccelerationModes = value; + this.OnPropertyChanged(); + } } public ListCollectionView OutputTypes @@ -285,10 +296,36 @@ private set public ICommand RemoveSelectedPresetCommand => this.removePresetCommand; + public ICommand RefreshDependencyHealthCommand => this.refreshDependencyHealthCommand; + + public ICommand RepairShellExtensionCommand => this.repairShellExtensionCommand; + public ICommand SaveCommand => this.saveCommand; public ICommand CloseCommand => this.closeCommand; + public ObservableCollection DependencyStatuses + { + get => this.dependencyStatuses; + + private set + { + this.dependencyStatuses = value; + this.OnPropertyChanged(); + } + } + + public string ShellExtensionRepairStatus + { + get => this.shellExtensionRepairStatus; + + private set + { + this.shellExtensionRepairStatus = value; + this.OnPropertyChanged(); + } + } + public TreeViewSelectionBehavior.IsChildOfPredicate PresetsHierarchyPredicate => (object nodeA, object nodeB) => { if (nodeA is PresetNode) @@ -405,6 +442,144 @@ private void OpenUrl(string url) } } + private void RefreshDependencyHealth() + { + ObservableCollection statuses = new ObservableCollection(); + + string shellExtensionPath = Helpers.GetDefaultShellExtensionPath(); + string defaultSettingsPath = FileConverterExtension.PathHelpers.DefaultSettingsFilePath; + string userSettingsPath = FileConverterExtension.PathHelpers.UserSettingsFilePath; + + this.AddFileStatus(statuses, "FFmpeg", GetApplicationFilePath("ffmpeg.exe"), "Required for audio and video conversions."); + this.AddFileStatus(statuses, "ImageMagick", GetApplicationFilePath("Magick.NET-Q16-AnyCPU.dll"), "Required for image, AVIF, PDF image, and WebP workflows."); + this.AddFileStatus(statuses, "ImageMagick native", GetApplicationFilePath("Magick.Native-Q16-x64.dll"), "Required native image processing runtime."); + this.AddFileStatus(statuses, "Ghostscript", GetApplicationFilePath("gswin64c.exe"), "Required for PDF rendering."); + this.AddFileStatus(statuses, "Ghostscript DLL", GetApplicationFilePath("gsdll64.dll"), "Required by ImageMagick PDF rendering."); + this.AddFileStatus(statuses, "Explorer extension DLL", shellExtensionPath, "Required for Windows Explorer right-click commands."); + this.AddFileStatus(statuses, "Default presets", defaultSettingsPath, "Required when creating or repairing user settings."); + + if (File.Exists(userSettingsPath)) + { + statuses.Add(new DependencyStatusViewModel("User settings", "Ready", userSettingsPath, true)); + } + else + { + statuses.Add(new DependencyStatusViewModel("User settings", "Will be created", userSettingsPath, true)); + } + + this.AddOfficeStatus(statuses, "Microsoft Word", ConversionJob_Office.ApplicationName.Word, "Required for Word document conversion."); + this.AddOfficeStatus(statuses, "Microsoft Excel", ConversionJob_Office.ApplicationName.Excel, "Required for spreadsheet conversion."); + this.AddOfficeStatus(statuses, "Microsoft PowerPoint", ConversionJob_Office.ApplicationName.PowerPoint, "Required for presentation conversion."); + this.AddShellRegistrationStatus(statuses); + + this.DependencyStatuses = statuses; + } + + private void RepairShellExtension() + { + string shellExtensionPath = Helpers.GetDefaultShellExtensionPath(); + if (!File.Exists(shellExtensionPath)) + { + this.ShellExtensionRepairStatus = $"Can't repair Explorer integration because {shellExtensionPath} is missing."; + this.RefreshDependencyHealth(); + return; + } + + string executablePath = Assembly.GetExecutingAssembly().Location; + ProcessStartInfo startInfo = new ProcessStartInfo(executablePath) + { + Arguments = $"--repair-shell-extension {QuoteArgument(shellExtensionPath)}", + UseShellExecute = true, + Verb = "runas", + }; + + try + { + Process.Start(startInfo); + this.ShellExtensionRepairStatus = "Repair launched with administrator privileges. Reopen Explorer or retry the context menu after it finishes."; + } + catch (Win32Exception exception) + { + if (exception.NativeErrorCode == 1223) + { + this.ShellExtensionRepairStatus = "Repair canceled by user."; + } + else + { + this.ShellExtensionRepairStatus = $"Repair failed to start: {exception.Message}"; + } + } + catch (Exception exception) + { + this.ShellExtensionRepairStatus = $"Repair failed to start: {exception.Message}"; + } + + this.RefreshDependencyHealth(); + } + + private void AddFileStatus(ObservableCollection statuses, string name, string path, string purpose) + { + if (!string.IsNullOrEmpty(path) && File.Exists(path)) + { + statuses.Add(new DependencyStatusViewModel(name, "Ready", $"{purpose} Found at {path}", true)); + } + else + { + statuses.Add(new DependencyStatusViewModel(name, "Missing", $"{purpose} Expected at {path ?? "(unknown path)"}", false)); + } + } + + private void AddOfficeStatus(ObservableCollection statuses, string name, ConversionJob_Office.ApplicationName applicationName, string purpose) + { + bool isAvailable = Helpers.IsMicrosoftOfficeApplicationAvailable(applicationName); + statuses.Add(new DependencyStatusViewModel( + name, + isAvailable ? "Available" : "Optional missing", + purpose, + true)); + } + + private void AddShellRegistrationStatus(ObservableCollection statuses) + { + string registeredPath = FileConverterExtension.PathHelpers.FileConverterPath; + string executablePath = Assembly.GetExecutingAssembly().Location; + + if (string.IsNullOrEmpty(registeredPath)) + { + statuses.Add(new DependencyStatusViewModel("Explorer registration", "Needs repair", "No executable path is registered in HKCU\\Software\\FileConverter.", false)); + return; + } + + if (!File.Exists(registeredPath)) + { + statuses.Add(new DependencyStatusViewModel("Explorer registration", "Needs repair", $"Registered executable is missing: {registeredPath}", false)); + return; + } + + bool matchesCurrentExecutable = string.Equals(registeredPath, executablePath, StringComparison.OrdinalIgnoreCase); + statuses.Add(new DependencyStatusViewModel( + "Explorer registration", + matchesCurrentExecutable ? "Ready" : "Different install", + matchesCurrentExecutable ? registeredPath : $"Registered path: {registeredPath}; current path: {executablePath}", + matchesCurrentExecutable)); + } + + private static string QuoteArgument(string value) + { + return $"\"{value.Replace("\"", "\\\"")}\""; + } + + private static string GetApplicationFilePath(string fileName) + { + string applicationFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (string.IsNullOrEmpty(applicationFolder)) + { + return fileName; + } + + return Path.Combine(applicationFolder, fileName); + } + private void InitializeCompatibleInputExtensions() { List categories = new List(); diff --git a/Application/FileConverter/Views/DiagnosticsWindow.xaml b/Application/FileConverter/Views/DiagnosticsWindow.xaml index d0eb5eba..f5661838 100644 --- a/Application/FileConverter/Views/DiagnosticsWindow.xaml +++ b/Application/FileConverter/Views/DiagnosticsWindow.xaml @@ -28,10 +28,14 @@ + +