ClipboardHelper.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. using System.Diagnostics;
  2. using System.Runtime.InteropServices;
  3. using Avalonia;
  4. using Avalonia.Controls.ApplicationLifetimes;
  5. using Avalonia.Input;
  6. using Avalonia.Input.Platform;
  7. using Avalonia.Media.Imaging;
  8. using Avalonia.Platform.Storage;
  9. using PicView.Avalonia.Animations;
  10. using PicView.Avalonia.ImageHandling;
  11. using PicView.Avalonia.Navigation;
  12. using PicView.Avalonia.ViewModels;
  13. using PicView.Core.FileHandling;
  14. using PicView.Core.Localization;
  15. using PicView.Core.ProcessHandling;
  16. namespace PicView.Avalonia.Clipboard;
  17. /// <summary>
  18. /// Helper class for clipboard operations
  19. /// </summary>
  20. public static class ClipboardHelper
  21. {
  22. /// <summary>
  23. /// Duplicates the current file and navigates to it
  24. /// </summary>
  25. /// <param name="vm">The main view model</param>
  26. public static async Task Duplicate(MainViewModel vm)
  27. {
  28. if (!NavigationManager.CanNavigate(vm))
  29. {
  30. return;
  31. }
  32. vm.IsLoading = true;
  33. var oldPath = vm.FileInfo.FullName;
  34. var duplicatedPath = await FileHelper.DuplicateAndReturnFileNameAsync(oldPath, vm.FileInfo);
  35. if (string.IsNullOrWhiteSpace(duplicatedPath) || !File.Exists(duplicatedPath))
  36. {
  37. return;
  38. }
  39. await Task.WhenAll(AnimationsHelper.CopyAnimation(), NavigationManager.LoadPicFromFile(duplicatedPath, vm));
  40. }
  41. /// <summary>
  42. /// Copies text to the clipboard
  43. /// </summary>
  44. /// <param name="text">The text to copy</param>
  45. /// <returns>A task representing the asynchronous operation</returns>
  46. public static async Task<bool> CopyTextToClipboard(string text)
  47. {
  48. var clipboard = GetClipboard();
  49. if (clipboard == null || string.IsNullOrWhiteSpace(text))
  50. {
  51. return false;
  52. }
  53. try
  54. {
  55. await Task.WhenAll(clipboard.SetTextAsync(text), AnimationsHelper.CopyAnimation());
  56. return true;
  57. }
  58. catch (Exception)
  59. {
  60. return false;
  61. }
  62. }
  63. /// <summary>
  64. /// Copies a file to the clipboard
  65. /// </summary>
  66. /// <param name="file">Path to the file</param>
  67. /// <param name="vm">The main view model</param>
  68. /// <returns>A task representing the asynchronous operation</returns>
  69. public static async Task<bool> CopyFileToClipboard(string? file, MainViewModel vm)
  70. {
  71. if (string.IsNullOrWhiteSpace(file))
  72. {
  73. return false;
  74. }
  75. try
  76. {
  77. var success = await Task.Run(() => vm.PlatformService.CopyFile(file));
  78. if (success)
  79. {
  80. await AnimationsHelper.CopyAnimation();
  81. }
  82. return success;
  83. }
  84. catch (Exception)
  85. {
  86. return false;
  87. }
  88. }
  89. /// <summary>
  90. /// Copies the current image to the clipboard
  91. /// </summary>
  92. /// <param name="vm">The main view model</param>
  93. /// <returns>A task representing the asynchronous operation</returns>
  94. public static async Task<bool> CopyImageToClipboard(MainViewModel vm)
  95. {
  96. var clipboard = GetClipboard();
  97. if (clipboard == null || vm.ImageSource is not Bitmap bitmap)
  98. {
  99. return false;
  100. }
  101. try
  102. {
  103. await clipboard.ClearAsync();
  104. // Handle for Windows
  105. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  106. {
  107. await Task.WhenAll(vm.PlatformService.CopyImageToClipboard(bitmap), AnimationsHelper.CopyAnimation());
  108. return false;
  109. }
  110. using var ms = new MemoryStream();
  111. bitmap.Save(ms);
  112. var dataObject = new DataObject();
  113. dataObject.Set("image/png", ms.ToArray());
  114. await Task.WhenAll(clipboard.SetDataObjectAsync(dataObject), AnimationsHelper.CopyAnimation());
  115. }
  116. catch (Exception)
  117. {
  118. return false;
  119. }
  120. return true;
  121. }
  122. /// <summary>
  123. /// Copies an image as base64 string to the clipboard
  124. /// </summary>
  125. /// <param name="path">Optional path to the image file</param>
  126. /// <param name="vm">The main view model</param>
  127. /// <returns>A task representing the asynchronous operation</returns>
  128. public static async Task<bool> CopyBase64ToClipboard(string path, MainViewModel vm)
  129. {
  130. var clipboard = GetClipboard();
  131. if (clipboard == null)
  132. {
  133. return false;
  134. }
  135. try
  136. {
  137. string base64;
  138. if (string.IsNullOrWhiteSpace(path))
  139. {
  140. switch (vm.ImageType)
  141. {
  142. case ImageType.AnimatedGif:
  143. case ImageType.AnimatedWebp:
  144. throw new ArgumentOutOfRangeException(nameof(vm.ImageType), "Animated images are not supported");
  145. case ImageType.Bitmap:
  146. if (vm.ImageSource is not Bitmap bitmap)
  147. {
  148. return false;
  149. }
  150. using (var stream = new MemoryStream())
  151. {
  152. bitmap.Save(stream, quality: 100);
  153. base64 = Convert.ToBase64String(stream.ToArray());
  154. }
  155. break;
  156. case ImageType.Svg:
  157. return false;
  158. default:
  159. throw new ArgumentOutOfRangeException(nameof(vm.ImageType), $"Unsupported image type: {vm.ImageType}");
  160. }
  161. }
  162. else
  163. {
  164. base64 = Convert.ToBase64String(await File.ReadAllBytesAsync(path));
  165. }
  166. if (string.IsNullOrEmpty(base64))
  167. {
  168. return false;
  169. }
  170. await Task.WhenAll(clipboard.SetTextAsync(base64), AnimationsHelper.CopyAnimation());
  171. return true;
  172. }
  173. catch (Exception)
  174. {
  175. return false;
  176. }
  177. }
  178. /// <summary>
  179. /// Cuts a file to the clipboard (copy + mark for deletion on paste)
  180. /// </summary>
  181. /// <param name="path">Path to the file</param>
  182. /// <param name="vm">The main view model</param>
  183. /// <returns>A task representing the asynchronous operation</returns>
  184. public static async Task<bool> CutFile(string path, MainViewModel vm)
  185. {
  186. if (string.IsNullOrWhiteSpace(path))
  187. {
  188. return false;
  189. }
  190. try
  191. {
  192. var success = await Task.Run(() => vm.PlatformService.CutFile(path));
  193. if (success)
  194. {
  195. await AnimationsHelper.CopyAnimation();
  196. }
  197. return success;
  198. }
  199. catch (Exception)
  200. {
  201. return false;
  202. }
  203. }
  204. /// <summary>
  205. /// Pastes content from the clipboard
  206. /// </summary>
  207. /// <param name="vm">The main view model</param>
  208. /// <returns>A task representing the asynchronous operation</returns>
  209. public static async Task Paste(MainViewModel vm)
  210. {
  211. var clipboard = GetClipboard();
  212. if (clipboard == null)
  213. {
  214. return;
  215. }
  216. try
  217. {
  218. // Try to paste files first
  219. var files = await clipboard.GetDataAsync(DataFormats.Files);
  220. if (files != null)
  221. {
  222. await PasteFiles(files, vm);
  223. return;
  224. }
  225. // Try to paste text (URLs, file paths)
  226. var text = await clipboard.GetTextAsync();
  227. if (!string.IsNullOrWhiteSpace(text))
  228. {
  229. await NavigationManager.LoadPicFromStringAsync(text, vm).ConfigureAwait(false);
  230. return;
  231. }
  232. // Try to paste image data
  233. await PasteClipboardImage(vm, clipboard);
  234. }
  235. catch (Exception ex)
  236. {
  237. // Log or handle the exception
  238. Debug.WriteLine($"Paste operation failed: {ex.Message}");
  239. }
  240. }
  241. /// <summary>
  242. /// Pastes an image from the clipboard
  243. /// </summary>
  244. /// <param name="vm">The main view model</param>
  245. /// <param name="clipboard">The clipboard instance</param>
  246. /// <returns>A task representing the asynchronous operation</returns>
  247. public static async Task PasteClipboardImage(MainViewModel vm, IClipboard clipboard)
  248. {
  249. var name = TranslationHelper.Translation.ClipboardImage;
  250. var imageType = ImageType.Bitmap;
  251. // List of formats to try
  252. string[] formats = new[]
  253. {
  254. "PNG", "image/jpeg", "image/png", "image/bmp", "BMP",
  255. "JPG", "JPEG", "image/tiff", "GIF", "image/gif"
  256. };
  257. foreach (var format in formats)
  258. {
  259. var bitmap = await GetBitmapFromBytes(format);
  260. if (bitmap != null)
  261. {
  262. await UpdateImage.SetSingleImageAsync(bitmap, imageType, name, vm);
  263. return;
  264. }
  265. }
  266. // Windows-specific clipboard handling
  267. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  268. {
  269. var bitmap = await vm.PlatformService.GetImageFromClipboard();
  270. if (bitmap != null)
  271. {
  272. await UpdateImage.SetSingleImageAsync(bitmap, imageType, name, vm);
  273. }
  274. }
  275. async Task<Bitmap?> GetBitmapFromBytes(string format)
  276. {
  277. try
  278. {
  279. var data = await clipboard.GetDataAsync(format);
  280. if (data is byte[] dataBytes)
  281. {
  282. using var memoryStream = new MemoryStream(dataBytes);
  283. return new Bitmap(memoryStream);
  284. }
  285. }
  286. catch (Exception)
  287. {
  288. // Ignore format errors and try next format
  289. }
  290. return null;
  291. }
  292. }
  293. /// <summary>
  294. /// Handles pasting files from the clipboard
  295. /// </summary>
  296. private static async Task PasteFiles(object files, MainViewModel vm)
  297. {
  298. if (files is IEnumerable<IStorageItem> items)
  299. {
  300. var storageItems = items.ToArray();
  301. if (storageItems.Length > 0)
  302. {
  303. // Load the first file
  304. var firstFile = storageItems[0];
  305. var firstPath = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
  306. ? firstFile.Path.AbsolutePath
  307. : firstFile.Path.LocalPath;
  308. await NavigationManager.LoadPicFromStringAsync(firstPath, vm).ConfigureAwait(false);
  309. // Open consecutive files in a new process
  310. foreach (var file in storageItems.Skip(1))
  311. {
  312. var path = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
  313. ? file.Path.AbsolutePath
  314. : file.Path.LocalPath;
  315. ProcessHelper.StartNewProcess(path);
  316. }
  317. }
  318. }
  319. else if (files is IStorageItem singleFile)
  320. {
  321. var path = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
  322. ? singleFile.Path.AbsolutePath
  323. : singleFile.Path.LocalPath;
  324. await NavigationManager.LoadPicFromStringAsync(path, vm).ConfigureAwait(false);
  325. }
  326. }
  327. /// <summary>
  328. /// Gets the clipboard instance from the current application
  329. /// </summary>
  330. /// <returns>The clipboard instance or null if not available</returns>
  331. private static IClipboard? GetClipboard()
  332. {
  333. if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  334. {
  335. return desktop.MainWindow.Clipboard;
  336. }
  337. return null;
  338. }
  339. }