Browse Source

Successfully register and unregister file associations on Windows #157

Ruben 7 months ago
parent
commit
9099dfd0a5

+ 6 - 1
src/PicView.Avalonia.MacOS/App.axaml.cs

@@ -481,6 +481,11 @@ public class App : Application, IPlatformSpecificService
 
     public string DefaultJsonKeyMap()
     {
-     return   MacOsKeybindings.DefaultKeybindings;
+        return MacOsKeybindings.DefaultKeybindings;
+    }
+
+    public Task AssociateFileTypes(string s)
+    {
+        throw new NotImplementedException();
     }
 }

+ 10 - 0
src/PicView.Avalonia.MacOS/Views/SettingsWindow.axaml.cs

@@ -3,7 +3,9 @@ using Avalonia.Input;
 using Avalonia.Media;
 using PicView.Avalonia.UI;
 using PicView.Core.Calculations;
+using PicView.Core.FileHandling;
 using PicView.Core.Localization;
+using PicView.Core.MacOS.FileAssociation;
 
 namespace PicView.Avalonia.MacOS.Views;
 
@@ -42,6 +44,8 @@ public partial class SettingsWindow : Window
             Hide();
             await SaveSettingsAsync();
         };
+        
+        InitializeFileAssociationManager();
     }
 
     private void MoveWindow(object? sender, PointerPressedEventArgs e)
@@ -51,4 +55,10 @@ public partial class SettingsWindow : Window
         var hostWindow = (Window)VisualRoot;
         hostWindow?.BeginMoveDrag(e);
     }
+    
+    private static void InitializeFileAssociationManager()
+    {
+        var iIFileAssociationService = new MacFileAssociationService();
+        FileAssociationManager.Initialize(iIFileAssociationService);
+    }
 }

+ 11 - 0
src/PicView.Avalonia.Win32/Views/SettingsWindow.axaml.cs

@@ -5,7 +5,9 @@ using Avalonia.Interactivity;
 using Avalonia.Media;
 using PicView.Avalonia.UI;
 using PicView.Core.Calculations;
+using PicView.Core.FileHandling;
 using PicView.Core.Localization;
+using PicView.Core.WindowsNT.FileAssociation;
 
 namespace PicView.Avalonia.Win32.Views;
 
@@ -73,6 +75,8 @@ public partial class SettingsWindow : Window
             Hide();
             await SaveSettingsAsync();
         };
+
+        InitializeFileAssociationManager();
     }
 
     private void MoveWindow(object? sender, PointerPressedEventArgs e)
@@ -92,4 +96,11 @@ public partial class SettingsWindow : Window
     {
         WindowState = WindowState.Minimized;
     }
+
+    private static void InitializeFileAssociationManager()
+    {
+        var iIFileAssociationService = new WindowsFileAssociationService();
+        FileAssociationManager.Initialize(iIFileAssociationService);
+    }
+
 }

+ 21 - 6
src/PicView.Avalonia/StartUp/StartUpHelper.cs

@@ -15,6 +15,7 @@ using PicView.Avalonia.ViewModels;
 using PicView.Avalonia.Views;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.Calculations;
+using PicView.Core.FileAssociations;
 using PicView.Core.Gallery;
 using PicView.Core.Navigation;
 using PicView.Core.ProcessHandling;
@@ -48,13 +49,27 @@ public static class StartUpHelper
                     Task.Run(async () => await UpdateManager.UpdateCurrentVersion(vm));
                     return;
                 }
-                if (arg.StartsWith("lockscreen", StringComparison.InvariantCultureIgnoreCase))
+                if (arg.StartsWith("associate:", StringComparison.OrdinalIgnoreCase) ||
+                    arg.StartsWith("unassociate:", StringComparison.OrdinalIgnoreCase))
                 {
-                    // var path = arg[(arg.LastIndexOf(',') + 1)..];
-                    // path = Path.GetFullPath(path);
-                    // vm.PlatformService.SetAsLockScreen(path);
-                    // Environment.Exit(0);
-                    return;
+                    for (int i = 1; i < args.Length; i++)
+                    {
+                        var currentArg = args[i];
+                        if (currentArg.StartsWith("associate:", StringComparison.OrdinalIgnoreCase) || 
+                            currentArg.StartsWith("unassociate:", StringComparison.OrdinalIgnoreCase))
+                        {
+                            Task.Run(async () =>
+                            {
+                                // Use the helper to process the association commands
+                                await FileTypeHelper.ProcessFileAssociationArguments(currentArg);
+                                if (args.Length <= 2)
+                                {
+                                    Environment.Exit(0);
+                                }
+                            });
+                        }
+                    }
+                    Environment.Exit(0);
                 }
             }
         }

+ 1 - 1
src/PicView.Core.WindowsNT/FileAssociation/FileAssociationHelper.cs → src/PicView.Core.WindowsNT/FileAssociation/WindowsFileAssociation.cs

@@ -6,7 +6,7 @@ using System.Diagnostics;
 
 namespace PicView.Core.WindowsNT.FileAssociation;
 
-public static class FileAssociationHelper
+public static class WindowsFileAssociation
 {
     public static bool RegisterFileAssociation(string extension, string description)
     {

+ 3 - 3
src/PicView.Core.WindowsNT/FileAssociation/WindowsFileAssociationService.cs

@@ -8,11 +8,11 @@ namespace PicView.Core.WindowsNT.FileAssociation;
 public class WindowsFileAssociationService : IFileAssociationService
 {
     public async Task<bool> RegisterFileAssociation(string extension, string description) =>
-        await Task.Run(() => FileAssociationHelper.RegisterFileAssociation(extension, description));
+        await Task.Run(() => WindowsFileAssociation.RegisterFileAssociation(extension, description));
     
     public async Task<bool> UnregisterFileAssociation(string extension) =>
-        await Task.Run(() => FileAssociationHelper.UnregisterFileAssociation(extension));
+        await Task.Run(() => WindowsFileAssociation.UnregisterFileAssociation(extension));
     
     public async Task<bool> IsFileAssociated(string extension) =>
-        await Task.Run(() => FileAssociationHelper.IsFileAssociated(extension));
+        await Task.Run(() => WindowsFileAssociation.IsFileAssociated(extension));
 }

+ 130 - 12
src/PicView.Core/FileAssociations/FileTypeHelper.cs

@@ -1,4 +1,7 @@
 using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
 using PicView.Core.FileHandling;
 using PicView.Core.Localization;
 
@@ -85,25 +88,71 @@ public static class FileTypeHelper
         return groups;
     }
     
-    public static async Task SetFileAssociations(ReadOnlyObservableCollection<FileTypeGroup> groups)
+   
+    public static async Task<bool> SetFileAssociations(ReadOnlyObservableCollection<FileTypeGroup> groups)
     {
-        foreach (var group in groups)
+        try
         {
-            foreach (var fileType in group.FileTypes)
+            // If we're on Windows, check for admin permissions
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !IsAdministrator())
             {
-                foreach (var extension in fileType.Extensions)
+                // Build list of extensions to associate
+                var extensionsToAssociate = new List<string>();
+                
+                foreach (var group in groups)
                 {
-                    // Make sure to properly handle extensions that contain commas
-                    var individualExtensions = extension.Split([',', ' '], StringSplitOptions.RemoveEmptyEntries);
-    
-                    foreach (var ext in individualExtensions)
+                    foreach (var fileType in group.FileTypes)
                     {
-                        var cleanExt = ext.Trim();
-                        if (!cleanExt.StartsWith('.'))
-                            cleanExt = "." + cleanExt;
+                        if (!fileType.IsSelected.HasValue || !fileType.IsSelected.Value) 
+                            continue;
+                        
+                        foreach (var extension in fileType.Extensions)
+                        {
+                            // Make sure to properly handle extensions that contain commas
+                            var individualExtensions = extension.Split([',', ' '], StringSplitOptions.RemoveEmptyEntries);
+        
+                            foreach (var ext in individualExtensions)
+                            {
+                                var cleanExt = ext.Trim();
+                                if (!cleanExt.StartsWith('.'))
+                                    cleanExt = "." + cleanExt;
+                                
+                                extensionsToAssociate.Add(cleanExt);
+                            }
+                        }
+                    }
+                }
+                
+                if (extensionsToAssociate.Count > 0)
+                {
+                    // Create command arguments - keep argument string shorter to avoid issues
+                    string associateArg = "associate:" + string.Join(",", extensionsToAssociate);
+                    
+                    // Start new process with elevated permissions
+                    return StartProcessWithElevatedPermission(associateArg);
+                }
+                
+                return true; // Nothing to do
+            }
             
-                        if (fileType.IsSelected.HasValue)
+            // Standard processing path (non-Windows or already has admin rights)
+            foreach (var group in groups)
+            {
+                foreach (var fileType in group.FileTypes)
+                {
+                    if (!fileType.IsSelected.HasValue) 
+                        continue;
+                    
+                    foreach (var extension in fileType.Extensions)
+                    {
+                        var individualExtensions = extension.Split([',', ' '], StringSplitOptions.RemoveEmptyEntries);
+        
+                        foreach (var ext in individualExtensions)
                         {
+                            var cleanExt = ext.Trim();
+                            if (!cleanExt.StartsWith('.'))
+                                cleanExt = "." + cleanExt;
+                
                             if (fileType.IsSelected.Value)
                                 await FileAssociationManager.AssociateFile(cleanExt);
                             else
@@ -112,6 +161,75 @@ public static class FileTypeHelper
                     }
                 }
             }
+            
+            return true;
+        }
+        catch (Exception ex)
+        {
+            // Log the exception or handle it appropriately
+            Debug.WriteLine($"Error in SetFileAssociations: {ex}");
+            return false;
+        }
+    }
+    
+    private static bool IsAdministrator()
+    {
+        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            return false;
+            
+        // Check if running as administrator
+        using var identity = WindowsIdentity.GetCurrent();
+        var principal = new WindowsPrincipal(identity);
+        return principal.IsInRole(WindowsBuiltInRole.Administrator);
+    }
+    
+    private static bool StartProcessWithElevatedPermission(string arguments)
+    {
+        try
+        {
+            var startInfo = new ProcessStartInfo
+            {
+                FileName = Process.GetCurrentProcess().MainModule?.FileName,
+                Arguments = arguments,
+                UseShellExecute = true,
+                Verb = "runas" // This requests elevated permissions
+            };
+            
+            Process.Start(startInfo);
+            return true;
+        }
+        catch (Exception ex)
+        {
+            // User declined the UAC prompt or other error
+            Debug.WriteLine($"Failed to start elevated process: {ex.Message}");
+            return false;
+        }
+    }
+    
+    public static async Task ProcessFileAssociationArguments(string arg)
+    {
+        try
+        {
+            if (arg.StartsWith("associate:", StringComparison.OrdinalIgnoreCase))
+            {
+                string extensionsString = arg["associate:".Length..];
+                if (string.IsNullOrWhiteSpace(extensionsString))
+                    return;
+                    
+                var extensions = extensionsString
+                    .Split(',', StringSplitOptions.RemoveEmptyEntries)
+                    .Select(ext => ext.Trim())
+                    .ToArray();
+                    
+                foreach (var extension in extensions)
+                {
+                    await FileAssociationManager.AssociateFile(extension);
+                }
+            }
+        }
+        catch (Exception ex)
+        {
+            Debug.WriteLine($"Error processing file association arguments: {ex}");
         }
     }
 }

+ 7 - 44
src/PicView.Core/FileHandling/FileAssociationManager.cs

@@ -15,32 +15,6 @@ public static class FileAssociationManager
     {
         _service = service ?? throw new ArgumentNullException(nameof(service));
     }
-    
-    /// <summary>
-    /// Associates all supported file extensions with the application
-    /// </summary>
-    public static async Task AssociateAllSupportedFiles()
-    {
-        EnsureInitialized();
-        
-        foreach (var ext in SupportedFiles.FileExtensions)
-        {
-            await _service.RegisterFileAssociation(ext, $"{ext.TrimStart('.')} Image File");
-        }
-    }
-    
-    /// <summary>
-    /// Removes all file associations for supported file extensions
-    /// </summary>
-    public static async Task UnassociateAllSupportedFiles()
-    {
-        EnsureInitialized();
-        
-        foreach (var ext in SupportedFiles.FileExtensions)
-        {
-            await _service.UnregisterFileAssociation(ext);
-        }
-    }
 
     /// <summary>
     /// Associates a single file extension with the application
@@ -56,8 +30,13 @@ public static class FileAssociationManager
     /// </summary>
     public static async Task<bool> UnassociateFile(string fileExtension)
     {
-        EnsureInitialized();
-        return await _service.UnregisterFileAssociation(fileExtension);
+        var isAssociated = await IsFileAssociated(fileExtension);
+        if (isAssociated)
+        {
+            return await _service.UnregisterFileAssociation(fileExtension);
+        }
+        
+        return false;
     }
     
     /// <summary>
@@ -69,22 +48,6 @@ public static class FileAssociationManager
         return await _service.IsFileAssociated(fileExtension);
     }
     
-    /// <summary>
-    /// Gets the association status of all supported file extensions
-    /// </summary>
-    public static async Task<Dictionary<string, bool>> GetAllAssociationStatus()
-    {
-        EnsureInitialized();
-        var result = new Dictionary<string, bool>();
-        
-        foreach (var ext in SupportedFiles.FileExtensions)
-        {
-            result[ext] = await _service.IsFileAssociated(ext);
-        }
-        
-        return result;
-    }
-    
     private static void EnsureInitialized()
     {
         if (_service == null)

+ 43 - 15
src/PicView.Core/ViewModels/FileAssociationsViewModel.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Reactive;
 using System.Reactive.Linq;
 using DynamicData;
@@ -20,7 +21,13 @@ public class FileAssociationsViewModel : ReactiveObject
         set => this.RaiseAndSetIfChanged(ref field, value);
     } = string.Empty;
 
-    public ReactiveCommand<Unit, Unit> ApplyCommand { get; }
+    private bool IsProcessing
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    }
+
+    public ReactiveCommand<Unit, bool> ApplyCommand { get; }
     public ReactiveCommand<Unit, string> ClearFilterCommand { get; }
     
     public FileAssociationsViewModel()
@@ -38,9 +45,26 @@ public class FileAssociationsViewModel : ReactiveObject
             .Filter(filter)
             .Bind(out _fileTypeGroups)
             .Subscribe();
+
+        // Canexecute for ApplyCommand
+        var canExecute = this.WhenAnyValue(x => x.IsProcessing)
+            .Select(processing => !processing);
+            
+        // Initialize commands with error handling
+        ApplyCommand = ReactiveCommand.CreateFromTask(
+            ApplyFileAssociations, 
+            canExecute);
+            
+        // Handle errors from the Apply command
+        ApplyCommand.ThrownExceptions
+            .Subscribe(ex => 
+            {
+                IsProcessing = false;
+#if DEBUG
+                Debug.WriteLine($"Error in ApplyCommand: {ex}");
+#endif
+            });
             
-        // Initialize commands
-        ApplyCommand = ReactiveCommand.CreateFromTask(async () => await ApplyFileAssociations());
         ClearFilterCommand = ReactiveCommand.Create(() => FilterText = string.Empty);
     }
     
@@ -88,13 +112,22 @@ public class FileAssociationsViewModel : ReactiveObject
         }
     }
 
-    private async Task ApplyFileAssociations()
+    private async Task<bool> ApplyFileAssociations()
     {
-        // Ensure all UI changes are synced to the ViewModel
-        SyncUIStateToViewModel();
-        
-        // Now process the associations
-        await FileTypeHelper.SetFileAssociations(FileTypeGroups);
+        try
+        {
+            IsProcessing = true;
+            
+            // Ensure all UI changes are synced to the ViewModel
+            SyncUIStateToViewModel();
+            
+            // Now process the associations
+            return await FileTypeHelper.SetFileAssociations(FileTypeGroups);
+        }
+        finally
+        {
+            IsProcessing = false;
+        }
     }
     
     public void InitializeFileTypes()
@@ -107,9 +140,4 @@ public class FileAssociationsViewModel : ReactiveObject
             list.AddRange(groups);
         });
     }
-}
-
-
-
-
-
+}