瀏覽代碼

[Avalonia] Add preview for dragging and dropping images, URLs, zip files and directories

Ruben 1 年之前
父節點
當前提交
f53e09d500

+ 4 - 2
src/PicView.Avalonia.Win32/Views/WinMainWindow.axaml.cs

@@ -1,8 +1,6 @@
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Input;
-using PicView.Avalonia.Keybindings;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 using PicView.Core.Config;
@@ -46,6 +44,10 @@ public partial class WinMainWindow : Window
                         break;
                 }
             });
+            PointerExited += (_, _) =>
+            {
+                MainView.RemoveDragDropView();
+            };
         };
         if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
         {

+ 5 - 0
src/PicView.Avalonia.Win32/Views/WinTitleBar.axaml.cs

@@ -1,5 +1,6 @@
 using Avalonia.Controls;
 using Avalonia.Input;
+using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 
 namespace PicView.Avalonia.Win32.Views;
@@ -10,6 +11,10 @@ public partial class WinTitleBar : UserControl
     {
         InitializeComponent();
         PointerPressed += (_, e) => MoveWindow(e);
+        PointerExited += (_, _) =>
+        {
+            UIHelper.GetMainView.RemoveDragDropView();
+        };
     }
 
     private void MoveWindow(PointerPressedEventArgs e)

+ 1 - 1
src/PicView.Avalonia/ImageHandling/ImageHelper.cs

@@ -232,7 +232,7 @@ public static class ImageHelper
     
     #region Thumbnail
 
-    private static async Task<Bitmap?> GetThumbAsync(string path, int height, FileInfo? fileInfo = null)
+    public static async Task<Bitmap?> GetThumbAsync(string path, int height, FileInfo? fileInfo = null)
     {
         try
         {

+ 0 - 38
src/PicView.Avalonia/Navigation/NavigationHelper.cs

@@ -303,44 +303,6 @@ public static class NavigationHelper
         FileHistoryNavigation.Add(url);
 
         vm.IsLoading = false;
-        
-//         var ext = Path.GetExtension(destination);
-//         if (string.IsNullOrEmpty(ext))
-//         {
-//             using var magickImage = new MagickImage();
-//             magickImage.Ping(fileInfo);
-//             ext = magickImage.Format.ToString();
-//             if (!string.IsNullOrEmpty(ext))
-//             {
-//                 var gif = ext.Equals("gif", StringComparison.InvariantCultureIgnoreCase);
-//                 var webp = ext.Equals("webp", StringComparison.InvariantCultureIgnoreCase);
-//                 if (gif || webp)
-//                 {
-//                     try
-//                     {
-//                         using var magickCollection = new MagickImageCollection();
-//                         magickCollection.Read(destination);
-//                         if (magickCollection.Count > 1)
-//                         {
-//                             if (gif)
-//                             {
-//                                 SetSingleImage(imageModel.Image, ImageType.AnimatedGif, url, vm);
-//                             }
-//                             else if (webp)
-//                             {
-//                                 SetSingleImage(imageModel.Image, ImageType.AnimatedWebp, url, vm);
-//                             }
-//                         }
-//                     }
-//                     catch (Exception e)
-//                     {
-// #if DEBUG
-//                         Console.WriteLine(e);
-// #endif
-//                     }
-//                 }
-//             }
-//         }
     }
     
     /// <summary>

+ 171 - 0
src/PicView.Avalonia/UI/DragAndDropHelper.cs

@@ -0,0 +1,171 @@
+using System.Runtime.InteropServices;
+using System.Text;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Platform.Storage;
+using Avalonia.Threading;
+using PicView.Avalonia.ImageHandling;
+using PicView.Avalonia.Navigation;
+using PicView.Avalonia.ViewModels;
+using PicView.Avalonia.Views.UC;
+using PicView.Core.Config;
+using PicView.Core.FileHandling;
+using PicView.Core.ProcessHandling;
+
+namespace PicView.Avalonia.UI;
+
+
+public static class DragAndDropHelper
+{
+    private static DragDropView? _dragDropView;
+    
+    public static async Task Drop(DragEventArgs e, MainViewModel vm)
+    {
+        RemoveDragDropView();
+
+        var files = e.Data.GetFiles();
+        if (files == null)
+        {
+            await HandleDropFromUrl(e, vm);
+            return;
+        }
+
+        var storageItems = files as IStorageItem[] ?? files.ToArray();
+        var firstFile = storageItems.FirstOrDefault();
+        var path = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? firstFile.Path.AbsolutePath : firstFile.Path.LocalPath;
+        await NavigationHelper.LoadPicFromStringAsync(path, vm).ConfigureAwait(false);
+        if (!SettingsHelper.Settings.UIProperties.OpenInSameWindow)
+        {
+            foreach (var file in storageItems.Skip(1))
+            {
+                var filepath = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? file.Path.AbsolutePath : file.Path.LocalPath;
+                ProcessHelper.StartNewProcess(filepath);
+            }
+        }
+    }
+    
+    private static async Task HandleDropFromUrl(DragEventArgs e, MainViewModel vm)
+    {
+        var urlObject = e.Data.Get("text/x-moz-url");
+        if (urlObject is byte[] bytes)
+        {
+            var dataStr = Encoding.Unicode.GetString(bytes);
+            var url = dataStr.Split((char)10).FirstOrDefault();
+            if (url != null)
+            {
+                await NavigationHelper.LoadPicFromUrlAsync(url, vm).ConfigureAwait(false);
+            }
+        }
+    }
+    
+    public static async Task DragEnter(DragEventArgs e, MainViewModel vm, Control control)
+    {
+
+        var files = e.Data.GetFiles();
+        if (files == null)
+        {
+            await HandleDragEnterFromUrl(e, vm);
+            return;
+        }
+
+        await HandleDragEnter(files, vm, control);
+    }
+
+    private static async Task HandleDragEnter(IEnumerable<IStorageItem> files, MainViewModel vm, Control control)
+    {
+        var fileArray = files as IStorageItem[] ?? files.ToArray();
+        if (fileArray is null || fileArray.Length < 1)
+        {
+            RemoveDragDropView();
+            return;
+        }
+        await Dispatcher.UIThread.InvokeAsync(() =>
+        {
+            if (_dragDropView == null)
+            {
+                _dragDropView = new DragDropView
+                {
+                    DataContext = vm
+                };
+                if (!control.IsPointerOver)
+                {
+                   UIHelper.GetMainView.MainGrid.Children.Add(_dragDropView);
+                }
+            }
+            else
+            {
+                _dragDropView.RemoveThumbnail();
+            }
+        });
+        var firstFile = fileArray[0];
+        var path = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? firstFile.Path.AbsolutePath : firstFile.Path.LocalPath;
+        if (Directory.Exists(path))
+        {
+            await Dispatcher.UIThread.InvokeAsync(() =>
+            {
+                if (!control.IsPointerOver)
+                {
+                    _dragDropView.AddDirectoryIcon();
+                }
+            });
+        }
+        else
+        {
+            if (path.IsArchive())
+            {
+                if (!control.IsPointerOver)
+                {
+                    _dragDropView.AddZipIcon();
+                }
+            }
+            else if (path.IsSupported())
+            {
+                var thumb = await ImageHelper.GetThumbAsync(path, 340).ConfigureAwait(false);
+
+                await Dispatcher.UIThread.InvokeAsync(() =>
+                {
+                    _dragDropView?.UpdateThumbnail(thumb);
+                });
+            }
+            else
+            {
+                RemoveDragDropView();
+            }
+        }
+    }
+    
+    private static async Task HandleDragEnterFromUrl(DragEventArgs e, MainViewModel vm)
+    {
+        var urlObject = e.Data.Get("text/x-moz-url");
+        if (urlObject != null)
+        {
+            await Dispatcher.UIThread.InvokeAsync(() =>
+            {
+                if (_dragDropView == null)
+                {
+                    _dragDropView = new DragDropView { DataContext = vm };
+                    _dragDropView.AddLinkChain();
+                    UIHelper.GetMainView.MainGrid.Children.Add(_dragDropView);
+                }
+                else
+                {
+                    _dragDropView.RemoveThumbnail();
+                }
+            });
+        }
+    }
+    
+    public static void DragLeave(DragEventArgs e, Control control)
+    {
+        if (!control.IsPointerOver)
+        {
+            RemoveDragDropView();
+        }
+    }
+    
+    public static void RemoveDragDropView()
+    {
+        UIHelper.GetMainView.MainGrid.Children.Remove(_dragDropView);
+        _dragDropView = null;
+    }
+}

+ 1 - 1
src/PicView.Avalonia/UI/UIHelper.cs

@@ -132,7 +132,7 @@ public static class UIHelper
     public static void AddToolTipMessage()
     {
         var mainView = GetMainView;
-        var toolTipMessage = new Views.UC.ToolTipMessage
+        var toolTipMessage = new ToolTipMessage
         {
             VerticalAlignment = VerticalAlignment.Center,
             HorizontalAlignment = HorizontalAlignment.Center,

+ 5 - 0
src/PicView.Avalonia/Views/BottomBar.axaml.cs

@@ -3,6 +3,7 @@ using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Media;
 using Avalonia.Styling;
+using PicView.Avalonia.UI;
 
 namespace PicView.Avalonia.Views;
 
@@ -48,6 +49,10 @@ public partial class BottomBar : UserControl
             PrevIcon.Fill = brush;
         };
         PointerPressed += (_, e) => MoveWindow(e);
+        PointerExited += (_, _) =>
+        {
+            DragAndDropHelper.RemoveDragDropView();
+        };
     }
 
     private void MoveWindow(PointerPressedEventArgs e)

+ 143 - 9
src/PicView.Avalonia/Views/MainView.axaml.cs

@@ -1,33 +1,40 @@
 using System.Runtime.InteropServices;
+using System.Text;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Media;
 using Avalonia.Platform.Storage;
 using Avalonia.Styling;
+using Avalonia.Threading;
+using PicView.Avalonia.ImageHandling;
 using PicView.Avalonia.Keybindings;
 using PicView.Avalonia.Navigation;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
+using PicView.Avalonia.Views.UC;
 using PicView.Core.Config;
 using PicView.Core.Extensions;
+using PicView.Core.FileHandling;
 using PicView.Core.ProcessHandling;
 
 namespace PicView.Avalonia.Views;
 
 public partial class MainView : UserControl
 {
+    private DragDropView? _dragDropView;
     public MainView()
     {
         InitializeComponent();
-        // TODO add visual feedback for drag and drop
 
         Loaded += delegate
         {
-            AddHandler(DragDrop.DragOverEvent, DragOver);
+            AddHandler(DragDrop.DragEnterEvent, DragEnter);
+            AddHandler(DragDrop.DragLeaveEvent, DragLeave);
             AddHandler(DragDrop.DropEvent, Drop);
 
             GotFocus += CloseTitlebarIfOpen;
+            LostFocus += HandleLostFocus;
             PointerPressed += PointerPressedBehavior;
 
             MainContextMenu.Opened += OnMainContextMenuOpened;
@@ -41,7 +48,7 @@ public partial class MainView : UserControl
         };
 
     }
-    
+
     private void PointerPressedBehavior(object? sender, PointerPressedEventArgs e)
     {
         CloseTitlebarIfOpen(sender, e);
@@ -52,6 +59,7 @@ public partial class MainView : UserControl
         }
         
         MainKeyboardShortcuts.ClearKeyDownModifiers();
+        RemoveDragDropView();
     }
     
     private void CloseTitlebarIfOpen(object? sender, EventArgs e)
@@ -65,6 +73,11 @@ public partial class MainView : UserControl
             vm.IsEditableTitlebarOpen = false;
         }
     }
+    
+    private void HandleLostFocus(object? sender, EventArgs e)
+    {
+        RemoveDragDropView();
+    }
 
     private void OnMainContextMenuOpened(object? sender, EventArgs e)
     {
@@ -132,17 +145,19 @@ public partial class MainView : UserControl
 
     private async Task Drop(object? sender, DragEventArgs e)
     {
+        RemoveDragDropView();
+        
         if (DataContext is not MainViewModel vm)
             return;
 
-        var data = e.Data.GetFiles();
-        if (data == null)
+        var files = e.Data.GetFiles();
+        if (files == null)
         {
-            // TODO Handle URL and folder drops
+            await HandleDropFromUrl(e, vm);
             return;
         }
 
-        var storageItems = data as IStorageItem[] ?? data.ToArray();
+        var storageItems = files as IStorageItem[] ?? files.ToArray();
         var firstFile = storageItems.FirstOrDefault();
         var path = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? firstFile.Path.AbsolutePath : firstFile.Path.LocalPath;
         await NavigationHelper.LoadPicFromStringAsync(path, vm).ConfigureAwait(false);
@@ -156,11 +171,130 @@ public partial class MainView : UserControl
         }
     }
     
-    private async Task DragOver(object? sender, DragEventArgs e)
+    private async Task HandleDropFromUrl(DragEventArgs e, MainViewModel vm)
+    {
+        var urlObject = e.Data.Get("text/x-moz-url");
+        if (urlObject is byte[] bytes)
+        {
+            var dataStr = Encoding.Unicode.GetString(bytes);
+            var url = dataStr.Split((char)10).FirstOrDefault();
+            if (url != null)
+            {
+                await NavigationHelper.LoadPicFromUrlAsync(url, vm).ConfigureAwait(false);
+            }
+        }
+    }
+    
+    private async Task DragEnter(object? sender, DragEventArgs e)
     {
         if (DataContext is not MainViewModel vm)
             return;
+
+        var files = e.Data.GetFiles();
+        if (files == null)
+        {
+            await HandleDragEnterFromUrl(e, vm);
+            return;
+        }
+
+        await HandleDragEnter(files, vm);
+    }
+
+    private async Task HandleDragEnter(IEnumerable<IStorageItem> files, MainViewModel vm)
+    {
+        var fileArray = files as IStorageItem[] ?? files.ToArray();
+        if (fileArray is null || fileArray.Length < 1)
+        {
+            RemoveDragDropView();
+            return;
+        }
+        await Dispatcher.UIThread.InvokeAsync(() =>
+        {
+            if (_dragDropView == null)
+            {
+                _dragDropView = new DragDropView
+                {
+                    DataContext = vm
+                };
+                if (!IsPointerOver)
+                {
+                    MainGrid.Children.Add(_dragDropView);
+                }
+            }
+            else
+            {
+                _dragDropView.RemoveThumbnail();
+            }
+        });
+        var firstFile = fileArray[0];
+        var path = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? firstFile.Path.AbsolutePath : firstFile.Path.LocalPath;
+        if (Directory.Exists(path))
+        {
+            await Dispatcher.UIThread.InvokeAsync(() =>
+            {
+                if (!IsPointerOver)
+                {
+                    _dragDropView.AddDirectoryIcon();
+                }
+            });
+        }
+        else
+        {
+            if (path.IsArchive())
+            {
+                if (!IsPointerOver)
+                {
+                    _dragDropView.AddZipIcon();
+                }
+            }
+            else if (path.IsSupported())
+            {
+                var thumb = await ImageHelper.GetThumbAsync(path, 340).ConfigureAwait(false);
+
+                await Dispatcher.UIThread.InvokeAsync(() =>
+                {
+                    _dragDropView?.UpdateThumbnail(thumb);
+                });
+            }
+            else
+            {
+                RemoveDragDropView();
+            }
+        }
+    }
     
-        
+    private async Task HandleDragEnterFromUrl(DragEventArgs e, MainViewModel vm)
+    {
+        var urlObject = e.Data.Get("text/x-moz-url");
+        if (urlObject != null)
+        {
+            await Dispatcher.UIThread.InvokeAsync(() =>
+            {
+                if (_dragDropView == null)
+                {
+                    _dragDropView = new DragDropView { DataContext = vm };
+                    _dragDropView.AddLinkChain();
+                    MainGrid.Children.Add(_dragDropView);
+                }
+                else
+                {
+                    _dragDropView.RemoveThumbnail();
+                }
+            });
+        }
+    }
+    
+    private void DragLeave(object? sender, DragEventArgs e)
+    {
+        if (!IsPointerOver)
+        {
+            RemoveDragDropView();
+        }
+    }
+    
+    public void RemoveDragDropView()
+    {
+        MainGrid.Children.Remove(_dragDropView);
+        _dragDropView = null;
     }
 }

文件差異過大導致無法顯示
+ 130 - 0
src/PicView.Avalonia/Views/UC/DirectoryIcon.axaml


+ 11 - 0
src/PicView.Avalonia/Views/UC/DirectoryIcon.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace PicView.Avalonia.Views.UC;
+
+public partial class DirectoryIcon : UserControl
+{
+    public DirectoryIcon()
+    {
+        InitializeComponent();
+    }
+}

+ 24 - 12
src/PicView.Avalonia/Views/UC/DragDrogView.axaml

@@ -1,24 +1,36 @@
 <UserControl
+    Background="{StaticResource AltBackgroundColor}"
+    IsHitTestVisible="False"
+    ZIndex="999"
     mc:Ignorable="d"
     x:Class="PicView.Avalonia.Views.UC.DragDropView"
     x:DataType="viewModels:MainViewModel"
     xmlns="https://github.com/avaloniaui"
+    xmlns:customControls="clr-namespace:PicView.Avalonia.CustomControls"
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     xmlns:viewModels="clr-namespace:PicView.Avalonia.ViewModels"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-    <Design.DataContext>
-        <viewModels:MainViewModel />
-    </Design.DataContext>
-    <Panel Background="{StaticResource BackgroundAlpha}">
-        <Border
-            BorderBrush="{StaticResource MainBorderColor}"
-            BorderThickness="1"
-            CornerRadius="8">
-            <Image
+    <Panel>
+        <customControls:BlurMask BlurRadius="9" />
+
+        <StackPanel x:Name="ParentPanel" HorizontalAlignment="Center" VerticalAlignment="Center">
+
+            <Border
+                ClipToBounds="True"
+                CornerRadius="6"
+                Height="340"
                 RenderOptions.BitmapInterpolationMode="HighQuality"
-                Stretch="Uniform"
-                x:Name="DragImage" />
-        </Border>
+                Width="600"
+                x:Name="ContentHolder" />
+
+            <TextBlock
+                Classes="txt"
+                FontFamily="/Assets/Fonts/Roboto-Black.ttf#Roboto"
+                FontSize="16"
+                Margin="0,20,0,0"
+                TextAlignment="Center"
+                x:Name="TxtDragToView" />
+        </StackPanel>
     </Panel>
 </UserControl>

+ 81 - 0
src/PicView.Avalonia/Views/UC/DragDrogView.axaml.cs

@@ -1,11 +1,92 @@
 using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using PicView.Avalonia.UI;
+using PicView.Avalonia.ViewModels;
+using PicView.Core.Localization;
 
 namespace PicView.Avalonia.Views.UC;
 
 public partial class DragDropView : UserControl
 {
+    private LinkChain? _linkChain;
+    private DirectoryIcon? _directoryIcon;
+    private ZipIcon? _zipIcon;
     public DragDropView()
     {
         InitializeComponent();
+        TxtDragToView.Text = TranslationHelper.Translation.DropToLoad;
+        Width = UIHelper.GetMainView.Bounds.Width;
+        Height = UIHelper.GetMainView.Bounds.Height;
+    }
+
+    public void AddLinkChain()
+    {
+        _linkChain ??= new LinkChain {  };
+        if (!ParentPanel.Children.Contains(_linkChain))
+        {
+            ParentPanel.Children.Add(_linkChain);
+            ParentPanel.Children.Move(ParentPanel.Children.IndexOf(_linkChain), 0);
+        }
+        ContentHolder.Background = null;
+        ContentHolder.IsVisible = false;
+    }
+
+    public void AddDirectoryIcon()
+    {
+        _directoryIcon ??= new DirectoryIcon();
+        if (!ParentPanel.Children.Contains(_directoryIcon))
+        {
+            ParentPanel.Children.Add(_directoryIcon);
+            ParentPanel.Children.Move(ParentPanel.Children.IndexOf(_directoryIcon), 0);
+        }
+        ContentHolder.Background = null;
+        ContentHolder.IsVisible = false;
+    }
+
+    public void AddZipIcon()
+    {
+        _zipIcon ??= new ZipIcon();
+        if (!ParentPanel.Children.Contains(_zipIcon))
+        {
+            ParentPanel.Children.Add(_zipIcon);
+            ParentPanel.Children.Move(ParentPanel.Children.IndexOf(_zipIcon), 0);
+        }
+        ContentHolder.Background = null;
+        ContentHolder.IsVisible = false;
+    }
+
+    public void UpdateThumbnail(Bitmap image)
+    {
+        TxtDragToView.Text = TranslationHelper.Translation.DropToLoad;
+        Width = UIHelper.GetMainView.Bounds.Width;
+        Height = UIHelper.GetMainView.Bounds.Height;
+        
+        if (DataContext is not MainViewModel vm)
+        {
+            return;
+        }
+
+        var screen = ScreenHelper.ScreenSize;
+        var padding = vm.BottombarHeight + vm.TitlebarHeight + 200;
+        var boxedWidth = UIHelper.GetMainView.Bounds.Width * screen.Scaling - padding;
+        var boxedHeight = UIHelper.GetMainView.Bounds.Height * screen.Scaling - padding;
+        var scaledWidth = boxedWidth / image?.PixelSize.Width ?? 600;
+        var scaledHeight = boxedHeight / image?.PixelSize.Height ?? 340;
+        var scale = Math.Min(scaledWidth, scaledHeight);
+        ContentHolder.Width = image?.PixelSize.Width * scale ?? 550;
+        ContentHolder.Height = image?.PixelSize.Height * scale ?? 340;
+        ContentHolder.Background = new ImageBrush
+        {
+            Opacity = 0.95,
+            Source = image
+        };
+        ContentHolder.IsVisible = true;
+    }
+
+    public void RemoveThumbnail()
+    {
+        ContentHolder.Background = null;
+        ParentPanel.Children.Remove(_linkChain);
     }
 }

+ 262 - 0
src/PicView.Avalonia/Views/UC/LinkChain.axaml

@@ -0,0 +1,262 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="PicView.Avalonia.Views.UC.LinkChain">
+    <Canvas
+        Width="512"
+        Height="512"
+        ClipToBounds="True">
+        <Canvas.Resources>
+            <PathGeometry x:Key="Mask1" Figures="M0 0L512 0L512 512L0 512L0 0Z">
+                <PathGeometry.Transform>
+                    <TranslateTransform X="-50.21198" Y="0" />
+                </PathGeometry.Transform>
+            </PathGeometry>
+        </Canvas.Resources>
+        <Rectangle Width="512" Height="512" />
+        <Canvas
+            Canvas.Left="50.21"
+            Width="411.58"
+            Height="512"
+            Clip="{StaticResource Mask1}"
+            ClipToBounds="True">
+            <Canvas
+                Canvas.Left="93.63"
+                Canvas.Top="121.66"
+                Width="233.06"
+                Height="232.99"
+                ClipToBounds="True">
+                <Canvas
+                    Width="233.06"
+                    Height="232.99"
+                    ClipToBounds="True">
+                    <Canvas
+                        Width="233.06"
+                        Height="232.99"
+                        ClipToBounds="True">
+                        <Path
+                            Width="233.06"
+                            Height="232.99"
+                            Data="M97.853 232.986C77.395 232.986 56.938 225.199 41.365 209.626L23.399 191.661C8.31 176.572 0 156.51 0 135.171C0 113.832 8.31 93.7713 23.399 78.6823L78.72 23.3603C109.869 -7.78675 160.55 -7.78675 191.698 23.3603L209.663 41.3252C224.752 56.4143 233.062 76.4762 233.062 97.8152C233.062 119.154 224.752 139.215 209.663 154.304L154.342 209.626C138.768 225.199 118.31 232.986 97.853 232.986L97.853 232.986Z"
+                            Stretch="Fill" />
+                        <Path
+                            Canvas.Left="44.93"
+                            Canvas.Top="44.91"
+                            Width="143.2"
+                            Height="143.17"
+                            Data="M90.279 0C81.326 0 72.376 3.407 65.56 10.222L10.239 65.544C3.636 72.146 0 80.925 0 90.262C0 99.6 3.636 108.379 10.239 114.981L28.204 132.946C41.834 146.575 64.012 146.575 77.641 132.946L132.962 77.624C139.565 71.022 143.201 62.243 143.201 52.906C143.201 43.569 139.565 34.789 132.962 28.187L114.997 10.222C108.183 3.409 99.23 0 90.279 0L90.279 0Z"
+                            Stretch="Fill" />
+                    </Canvas>
+                </Canvas>
+            </Canvas>
+            <Canvas
+                Canvas.Left="58.93"
+                Canvas.Top="237.15"
+                Width="233.06"
+                Height="233.06"
+                ClipToBounds="True">
+                <Canvas
+                    Width="233.06"
+                    Height="233.06"
+                    ClipToBounds="True">
+                    <Canvas
+                        Width="233.06"
+                        Height="233.06"
+                        ClipToBounds="True">
+                        <Path
+                            Width="233.06"
+                            Height="233.06"
+                            Data="M97.853 233.061C76.514 233.061 56.453 224.752 41.364 209.663L23.399 191.698C8.31001 176.609 0 156.548 0 135.209C0 113.87 8.31 93.808 23.398 78.719L78.72 23.398C93.809 8.30899 113.87 0 135.209 0C156.548 0 176.609 8.30901 191.698 23.399L209.663 41.363C224.752 56.452 233.062 76.513 233.062 97.852C233.062 119.191 224.752 139.253 209.664 154.342L154.342 209.663C139.254 224.752 119.192 233.061 97.853 233.061L97.853 233.061Z"
+                            Stretch="Fill" />
+                        <Path
+                            Canvas.Left="44.93"
+                            Canvas.Top="44.93"
+                            Width="143.2"
+                            Height="143.2"
+                            Data="M90.279 0C80.942 0 72.163 3.63599 65.561 10.239L10.239 65.56C3.63699 72.162 0 80.942 0 90.279C0 99.616 3.636 108.395 10.239 114.997L28.204 132.962C34.807 139.565 43.585 143.201 52.922 143.201C62.259 143.201 71.038 139.565 77.64 132.962L132.962 77.641C139.564 71.039 143.201 62.259 143.201 52.922C143.201 43.585 139.565 34.806 132.962 28.204L114.996 10.239C108.394 3.63602 99.616 0 90.279 0L90.279 0Z"
+                            Stretch="Fill" />
+                    </Canvas>
+                </Canvas>
+            </Canvas>
+            <Canvas
+                Canvas.Top="73.85"
+                Width="411.58"
+                Height="438.15"
+                ClipToBounds="True">
+                <Path
+                    Width="411.58"
+                    Height="438.15"
+                    Data="M4.992 2.28882e-05L0 11.871L0 407.088C0 424.243 13.907 438.15 31.062 438.15L380.515 438.15C397.67 438.15 411.577 424.243 411.577 407.088L411.577 11.871L406.585 0L4.992 2.28882e-05Z"
+                    Fill="#D8EAFB"
+                    Stretch="Fill" />
+            </Canvas>
+            <Canvas
+                Canvas.Top="73.85"
+                Width="411.58"
+                Height="438.15"
+                ClipToBounds="True">
+                <Path
+                    Width="411.58"
+                    Height="438.15"
+                    Data="M360.714 2.28882e-05L360.714 356.226C360.714 373.381 346.807 387.288 329.652 387.288L0 387.288L0 407.088C0 424.243 13.907 438.15 31.062 438.15L380.515 438.15C397.67 438.15 411.577 424.243 411.577 407.088L411.577 11.871L406.585 0L360.714 2.28882e-05Z"
+                    Stretch="Fill">
+                    <Path.Fill>
+                        <SolidColorBrush Opacity=".4" Color="{DynamicResource AccentColor}" />
+                    </Path.Fill>
+                </Path>
+            </Canvas>
+            <Canvas
+                Width="411.58"
+                Height="85.72"
+                ClipToBounds="True">
+                <Path
+                    Width="411.58"
+                    Height="85.72"
+                    Data="M380.514 0L31.062 0C13.907 0 0 13.907 0 31.062L0 85.722L411.576 85.722L411.576 31.062C411.576 13.907 397.669 0 380.514 0L380.514 0Z"
+                    Stretch="Fill">
+                    <Path.Fill>
+                        <SolidColorBrush Color="{DynamicResource AccentColor}" />
+                    </Path.Fill>
+                </Path>
+            </Canvas>
+            <Canvas
+                Canvas.Left="360.71"
+                Width="50.86"
+                Height="85.72"
+                ClipToBounds="True">
+                <Path
+                    Width="50.86"
+                    Height="85.72"
+                    Data="M19.8 0L0 0L0 85.721L50.862 85.721L50.862 31.061C50.862 13.907 36.955 0 19.8 0L19.8 0Z"
+                    Stretch="Fill">
+                    <Path.Fill>
+                        <SolidColorBrush Opacity=".7" Color="{DynamicResource AccentColor}" />
+                    </Path.Fill>
+                </Path>
+            </Canvas>
+            <Canvas
+                Canvas.Left="38.33"
+                Canvas.Top="28.93"
+                Width="113.61"
+                Height="27.87"
+                ClipToBounds="True">
+                <Canvas
+                    Width="27.87"
+                    Height="27.87"
+                    ClipToBounds="True">
+                    <Ellipse
+                        Width="27.87"
+                        Height="27.87"
+                        Fill="#B3D8FB" />
+                </Canvas>
+                <Canvas
+                    Canvas.Left="42.87"
+                    Width="27.87"
+                    Height="27.87"
+                    ClipToBounds="True">
+                    <Ellipse
+                        Width="27.87"
+                        Height="27.87"
+                        Fill="#B3D8FB" />
+                </Canvas>
+                <Canvas
+                    Canvas.Left="85.74"
+                    Width="27.87"
+                    Height="27.87"
+                    ClipToBounds="True">
+                    <Ellipse
+                        Width="27.87"
+                        Height="27.87"
+                        Fill="#B3D8FB" />
+                </Canvas>
+            </Canvas>
+            <Canvas
+                Canvas.Left="93.63"
+                Canvas.Top="121.66"
+                Width="233.06"
+                Height="232.98"
+                ClipToBounds="True">
+                <Canvas
+                    Width="233.06"
+                    Height="232.98"
+                    ClipToBounds="True">
+                    <Path
+                        Width="233.06"
+                        Height="232.98"
+                        Data="M233.063 97.8115C233.063 119.159 224.756 139.217 209.659 154.305L188.731 175.242C185.107 168.563 180.494 162.372 174.962 156.85L159.276 141.154L177.887 122.533C184.497 115.933 188.131 107.147 188.131 97.8115C188.131 88.4765 184.497 79.6995 177.887 73.0995L159.925 55.1275C153.115 48.3185 144.159 44.9135 135.203 44.9135C126.257 44.9135 117.301 48.3185 110.491 55.1275L55.166 110.452C48.566 117.052 44.932 125.828 44.932 135.174C44.932 136.072 44.962 136.971 45.042 137.87C45.661 146.187 49.206 153.925 55.166 159.886L70.852 175.582L73.128 177.859C79.947 184.678 88.894 188.083 97.85 188.083C106.806 188.083 115.752 184.678 122.572 177.859L127.504 172.926L143.19 188.622C149.151 194.583 152.695 202.311 153.314 210.629C137.868 225.536 117.859 232.984 97.85 232.984C77.392 232.984 56.933 225.196 41.357 209.63L39.08 207.353L23.394 191.658C17.863 186.135 13.25 179.945 9.625 173.266C3.33501 161.703 0 148.694 0 135.174C0 113.827 8.30701 93.7685 23.394 78.6815L78.719 23.3565C109.861 -7.7855 160.543 -7.7855 191.695 23.3565L209.657 41.3285C224.756 56.4155 233.063 76.4755 233.063 97.8115L233.063 97.8115Z"
+                        Fill="#7C8B96"
+                        Stretch="Fill" />
+                </Canvas>
+            </Canvas>
+            <Canvas
+                Canvas.Left="58.92"
+                Canvas.Top="237.14"
+                Width="233.06"
+                Height="233.06"
+                ClipToBounds="True">
+                <Canvas
+                    Width="233.06"
+                    Height="233.06"
+                    ClipToBounds="True">
+                    <Path
+                        Width="233.06"
+                        Height="233.06"
+                        Data="M233.061 97.851C233.061 119.188 224.754 139.257 209.667 154.344L154.342 209.668C139.255 224.755 119.196 233.062 97.859 233.062C76.512 233.062 56.453 224.755 41.366 209.668L23.404 191.696C8.307 176.609 0 156.551 0 135.213C0 113.866 8.307 93.807 23.404 78.72L44.332 57.782C47.956 64.462 52.569 70.652 58.101 76.174L73.787 91.869L55.176 110.49C48.566 117.09 44.932 125.876 44.932 135.212C44.932 144.548 48.566 153.324 55.176 159.924L73.138 177.896C76.433 181.201 80.287 183.757 84.471 185.484C88.655 187.221 93.188 188.13 97.86 188.13C107.196 188.13 115.972 184.495 122.572 177.896L177.897 122.571C184.497 115.971 188.131 107.185 188.131 97.849C188.131 96.94 188.101 96.042 188.021 95.143C187.402 86.826 183.857 79.098 177.897 73.137L162.211 57.441L159.935 55.165C153.325 48.565 144.549 44.931 135.213 44.931C125.877 44.931 117.101 48.565 110.491 55.165L105.559 60.097L89.873 44.401C83.912 38.44 80.368 30.702 79.749 22.385C94.7261 7.937 114.356 0 135.213 0C156.55 0 176.609 8.30701 191.706 23.394L193.983 25.671L209.669 41.367C215.2 46.889 219.813 53.079 223.438 59.759C229.726 71.322 233.061 84.332 233.061 97.851L233.061 97.851Z"
+                        Fill="#9CABB8"
+                        Stretch="Fill" />
+                </Canvas>
+            </Canvas>
+            <Canvas
+                Canvas.Left="117.78"
+                Canvas.Top="144.96"
+                Width="208.91"
+                Height="209.69"
+                ClipToBounds="True">
+                <Canvas
+                    Canvas.Left="149.64"
+                    Width="59.27"
+                    Height="151.95"
+                    ClipToBounds="True">
+                    <Path
+                        Width="59.27"
+                        Height="151.95"
+                        Data="M35.868 18.034L17.906 0.0619965C17.885 0.0410004 17.862 0.0220032 17.841 0L18.622 0.781006C33.711 15.87 42.021 35.932 42.021 57.271C42.021 78.61 33.711 98.671 18.622 113.76L0 132.383L1.172 133.555C6.703 139.077 11.316 145.267 14.941 151.947L35.869 131.009C50.966 115.922 59.273 95.863 59.273 74.516C59.271 53.18 50.964 33.12 35.868 18.034L35.868 18.034Z"
+                        Fill="#465961"
+                        Stretch="Fill" />
+                </Canvas>
+                <Canvas
+                    Canvas.Top="164.15"
+                    Width="129.16"
+                    Height="45.53"
+                    ClipToBounds="True">
+                    <Path
+                        Width="129.16"
+                        Height="45.53"
+                        Data="M117.868 0L112.94 4.92801C97.365 20.502 76.908 28.288 56.451 28.288C36.009 28.288 15.569 20.512 0 4.96298L14.931 19.903L17.208 22.18C32.784 37.746 53.242 45.534 73.701 45.534C93.71 45.534 113.719 38.086 129.165 23.179C128.546 14.862 125.001 7.13397 119.041 1.17297L117.868 0Z"
+                        Fill="#465961"
+                        Stretch="Fill" />
+                </Canvas>
+            </Canvas>
+            <Canvas
+                Canvas.Left="83.16"
+                Canvas.Top="260.4"
+                Width="208.82"
+                Height="209.8"
+                ClipToBounds="True">
+                <Canvas
+                    Width="208.82"
+                    Height="209.8"
+                    ClipToBounds="True">
+                    <Path
+                        Width="208.82"
+                        Height="209.8"
+                        Data="M199.197 36.502C195.573 29.822 190.96 23.632 185.428 18.11L169.742 2.414L167.465 0.136993C167.418 0.0899963 167.368 0.0469971 167.321 0L168.178 0.856995C183.267 15.946 191.577 36.007 191.577 57.346C191.577 78.685 183.267 98.747 168.179 113.836L112.857 169.157C97.768 184.246 77.707 192.555 56.368 192.555C35.085 192.555 15.075 184.286 0 169.272L17.128 186.409C32.215 201.496 52.274 209.803 73.621 209.803C94.958 209.803 115.017 201.496 130.104 186.409L185.429 131.085C200.516 115.998 208.823 95.929 208.823 74.592C208.822 61.074 205.487 48.064 199.197 36.502L199.197 36.502Z"
+                        Fill="#7C8B96"
+                        Stretch="Fill" />
+                </Canvas>
+            </Canvas>
+        </Canvas>
+    </Canvas>
+</UserControl>

+ 11 - 0
src/PicView.Avalonia/Views/UC/LinkChain.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace PicView.Avalonia.Views.UC;
+
+public partial class LinkChain : UserControl
+{
+    public LinkChain()
+    {
+        InitializeComponent();
+    }
+}

+ 120 - 0
src/PicView.Avalonia/Views/UC/ZipIcon.axaml

@@ -0,0 +1,120 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="PicView.Avalonia.Views.UC.ZipIcon">
+    <Canvas
+        Width="265"
+        Height="265"
+        ClipToBounds="True">
+        <Canvas.Resources>
+            <PathGeometry x:Key="Mask1" Figures="M0 0L265 0L265 265L0 265L0 0Z">
+                <PathGeometry.Transform>
+                    <TranslateTransform X="-30.75894" Y="0" />
+                </PathGeometry.Transform>
+            </PathGeometry>
+        </Canvas.Resources>
+        <Rectangle Width="265" Height="265" />
+        <Canvas
+            Canvas.Left="30.76"
+            Width="203.48"
+            Height="265"
+            Clip="{StaticResource Mask1}"
+            ClipToBounds="True">
+            <Path
+                Width="203.48"
+                Height="265"
+                Data="M144.259 0L6.92312 0C3.09955 0 0 3.09955 0 9.11411L0 260.268C0 261.9 3.09955 265 6.92313 265L196.559 265C200.383 265 203.482 261.9 203.482 260.268L203.482 61.4138C203.482 58.1202 203.042 57.0602 202.266 56.2794L147.203 1.21616C146.422 0.440089 145.362 0 144.259 0L144.259 0Z"
+                Fill="#E9E9E0"
+                Stretch="Fill" />
+            <Path
+                Canvas.Left="146.7"
+                Canvas.Top="0.71"
+                Width="56.07"
+                Height="56.07"
+                Data="M0 0L0 56.0712L56.0711 56.0712L0 0Z"
+                Fill="#D9D7CA"
+                Stretch="Fill" />
+            <Path
+                Canvas.Top="184.55"
+                Width="203.48"
+                Height="80.45"
+                Data="M196.559 80.4464L6.92312 80.4464C3.09955 80.4464 0 77.3469 0 73.5233L0 0L203.482 0L203.482 73.5233C203.482 77.3469 200.383 80.4464 196.559 80.4464L196.559 80.4464Z"
+                Fill="{DynamicResource AccentColor}"
+                Stretch="Fill" />
+            <Canvas
+                Canvas.Left="57.1"
+                Canvas.Top="203.12"
+                Width="92.84"
+                Height="47.68"
+                ClipToBounds="True">
+                <Path
+                    Width="31.7"
+                    Height="47.68"
+                    Data="M31.7006 0L31.7006 6.27482L8.99107 40.3699L7.69919 41.4063L31.7006 41.4063L31.7006 47.6811L0 47.6811L0 41.4063L22.7096 7.31117L24.0677 6.27482L0 6.27482L0 0L31.7006 0L31.7006 0Z"
+                    Fill="#FFFFFF"
+                    Stretch="Fill" />
+                <Rectangle
+                    Canvas.Left="42.76"
+                    Width="7.89"
+                    Height="47.68"
+                    Fill="#FFFFFF" />
+                <Canvas
+                    Canvas.Left="62.49"
+                    Width="30.34"
+                    Height="47.68"
+                    ClipToBounds="True">
+                    <Path
+                        Width="30.34"
+                        Height="47.68"
+                        Data="M7.76545 47.6811L0 47.6811L0 0L13.7138 0C15.7391 0 17.7455 0.321789 19.7283 0.970098C21.7111 1.61841 23.4904 2.58849 25.0662 3.88037C26.642 5.17224 27.9149 6.73859 28.885 8.56993C29.8551 10.4013 30.3425 12.4597 30.3425 14.7501C30.3425 17.1682 29.9308 19.3545 29.1121 21.3183C28.2935 23.2822 27.1483 24.9337 25.6813 26.2682C24.2144 27.6026 22.4445 28.639 20.3766 29.3724C18.3087 30.1059 16.0183 30.4703 13.5197 30.4703L7.76072 30.4703L7.76072 47.6811L7.76545 47.6811Z"
+                        Fill="White"
+                        Stretch="Fill" />
+                    <Path
+                        Canvas.Left="7.77"
+                        Canvas.Top="5.89"
+                        Width="15.07"
+                        Height="18.89"
+                        Data="M0 0L0 18.8907L7.11715 18.8907C8.06358 18.8907 9.00054 18.7298 9.93278 18.4033C10.8603 18.0815 11.7121 17.5515 12.4881 16.818C13.2642 16.0846 13.8889 15.0624 14.3621 13.7469C14.8353 12.4314 15.0719 10.8035 15.0719 8.86331C15.0719 8.08723 14.9631 7.18813 14.7501 6.18018C14.5324 5.1675 14.0923 4.19742 13.4251 3.2699C12.7532 2.34241 11.8162 1.56633 10.6095 0.941685C9.40278 0.317042 7.80333 0.00471149 5.82055 0.00471149L0 0.00471149L0 0Z"
+                        Fill="{DynamicResource AccentColor}"
+                        Stretch="Fill" />
+                </Canvas>
+            </Canvas>
+            <Canvas
+                Canvas.Left="75.71"
+                Canvas.Top="28.39"
+                Width="47.32"
+                Height="132.5"
+                ClipToBounds="True">
+                <Canvas
+                    Width="47.32"
+                    Height="132.5"
+                    ClipToBounds="True">
+                    <Path
+                        Width="47.32"
+                        Height="132.5"
+                        Data="M28.3929 85.1786L28.3929 75.7143L37.8571 75.7143L37.8571 66.25L28.3929 66.25L28.3929 56.7857L37.8571 56.7857L37.8571 47.3214L28.3929 47.3214L28.3929 37.8571L37.8571 37.8571L37.8571 28.3929L28.3929 28.3929L28.3929 18.9286L37.8571 18.9286L37.8571 9.46429L28.3929 9.46429L28.3929 0L18.9286 0L18.9286 9.46429L9.46429 9.46429L9.46429 18.9286L18.9286 18.9286L18.9286 28.3929L9.46429 28.3929L9.46429 37.8571L18.9286 37.8571L18.9286 47.3214L9.46429 47.3214L9.46429 56.7857L18.9286 56.7857L18.9286 66.25L9.46429 66.25L9.46429 75.7143L18.9286 75.7143L18.9286 85.1786L0 85.1786L0 108.839C0 121.886 10.6142 132.5 23.6607 132.5C36.7072 132.5 47.3214 121.886 47.3214 108.839L47.3214 85.1786L28.3929 85.1786L28.3929 85.1786Z"
+                        Fill="#C8BDB8"
+                        Stretch="Fill" />
+                    <Path
+                        Canvas.Left="9.46"
+                        Canvas.Top="94.64"
+                        Width="28.39"
+                        Height="28.39"
+                        Data="M28.3929 14.1964C28.3929 22.0234 22.0234 28.3929 14.1964 28.3929C6.36947 28.3929 0 22.0234 0 14.1964L0 0L28.3929 0L28.3929 14.1964L28.3929 14.1964Z"
+                        Fill="#E9E9E0"
+                        Stretch="Fill" />
+                </Canvas>
+                <Path
+                    Canvas.Left="14.2"
+                    Canvas.Top="104.11"
+                    Width="18.93"
+                    Height="9.46"
+                    Data="M4.73214 9.46429L14.1964 9.46429C16.8086 9.46429 18.9286 7.34902 18.9286 4.73214C18.9286 2.11527 16.8086 0 14.1964 0L4.73214 0C2.12 0 0 2.11527 0 4.73214C0 7.34902 2.12 9.46429 4.73214 9.46429L4.73214 9.46429Z"
+                    Fill="#C8BDB8"
+                    Stretch="Fill" />
+            </Canvas>
+        </Canvas>
+    </Canvas>
+</UserControl>

+ 11 - 0
src/PicView.Avalonia/Views/UC/ZipIcon.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace PicView.Avalonia.Views.UC;
+
+public partial class ZipIcon : UserControl
+{
+    public ZipIcon()
+    {
+        InitializeComponent();
+    }
+}

部分文件因文件數量過多而無法顯示