diff --git a/CollapseLauncher/Classes/Helper/WindowUtility.cs b/CollapseLauncher/Classes/Helper/WindowUtility.cs index 9e98f6314..0b9e24a36 100644 --- a/CollapseLauncher/Classes/Helper/WindowUtility.cs +++ b/CollapseLauncher/Classes/Helper/WindowUtility.cs @@ -53,9 +53,10 @@ internal enum WindowBackdropKind } internal delegate nint WndProcDelegate(nint hwnd, uint msg, nuint wParam, nint lParam); - internal static class WindowUtility + internal static partial class WindowUtility { private static event EventHandler? DragAreaChangeEvent; + internal static event Action? FileDropEvent; private static nint _oldMainWndProcPtr; private static nint _oldDesktopSiteBridgeWndProcPtr; @@ -417,6 +418,52 @@ internal static void RegisterWindow(this Window window) FileDialogNative.InitHandlerPointer(CurrentWindowPtr); } + internal static void SetFileDropEnabled(bool isEnabled) + { + const uint WM_COPYDATA = 0x004A; + const uint WM_COPYGLOBALDATA = 0x0049; + const uint WM_DROPFILES = 0x0233; + const uint MSGFLT_RESET = 0; + const uint MSGFLT_ALLOW = 1; + + nint windowHandle = CurrentWindowPtr; + if (windowHandle == nint.Zero) + { + return; + } + + uint messageFilterAction = isEnabled ? MSGFLT_ALLOW : MSGFLT_RESET; + NativeFileDrop.ChangeWindowMessageFilterEx(windowHandle, WM_DROPFILES, messageFilterAction, nint.Zero); + NativeFileDrop.ChangeWindowMessageFilterEx(windowHandle, WM_COPYDATA, messageFilterAction, nint.Zero); + NativeFileDrop.ChangeWindowMessageFilterEx(windowHandle, WM_COPYGLOBALDATA, messageFilterAction, nint.Zero); + NativeFileDrop.DragAcceptFiles(windowHandle, isEnabled); + } + + internal static bool TryGetExternalDragPosition(out PointInt32 dragPosition) + { + const int VK_LBUTTON = 0x01; + const int VK_RBUTTON = 0x02; + const int KeyPressed = 0x8000; + + dragPosition = default; + nint windowHandle = CurrentWindowPtr; + if (windowHandle == nint.Zero) + { + return false; + } + + bool isMouseButtonPressed = (NativeFileDrop.GetAsyncKeyState(VK_LBUTTON) & KeyPressed) != 0 || + (NativeFileDrop.GetAsyncKeyState(VK_RBUTTON) & KeyPressed) != 0; + if (!isMouseButtonPressed || !NativeFileDrop.GetCursorPos(out NativePoint cursorPosition)) + { + return false; + } + + NativeFileDrop.ScreenToClient(windowHandle, ref cursorPosition); + dragPosition = new PointInt32(cursorPosition.X, cursorPosition.Y); + return true; + } + #region Drag Area Handler private static void InstallDragAreaChangeMonitor() @@ -496,6 +543,7 @@ private static nint MainWndProc(nint hwnd, uint msg, nuint wParam, nint lParam) const uint WM_ACTIVATE = 0x0006; const uint WM_QUERYENDSESSION = 0x0011; const uint WM_ENDSESSION = 0x0016; + const uint WM_DROPFILES = 0x0233; switch (msg) { @@ -643,11 +691,88 @@ private static nint MainWndProc(nint hwnd, uint msg, nuint wParam, nint lParam) (CurrentWindow as MainWindow)?.CloseApp(); } break; + case WM_DROPFILES: + HandleFileDrop((nint)wParam); + return 0; } return PInvoke.CallWindowProc(_oldMainWndProcPtr, hwnd, msg, wParam, lParam); } + private static unsafe void HandleFileDrop(nint dropHandle) + { + const uint GetFileCount = uint.MaxValue; + + try + { + int fileCount = (int)NativeFileDrop.DragQueryFile(dropHandle, GetFileCount, null, 0); + string[] files = new string[fileCount]; + for (int i = 0; i < fileCount; i++) + { + uint filePathLength = NativeFileDrop.DragQueryFile(dropHandle, (uint)i, null, 0); + char[] filePathBuffer = new char[filePathLength + 1]; + fixed (char* filePathBufferPtr = filePathBuffer) + { + NativeFileDrop.DragQueryFile(dropHandle, (uint)i, filePathBufferPtr, (uint)filePathBuffer.Length); + files[i] = new string(filePathBufferPtr, 0, (int)filePathLength); + } + } + + NativeFileDrop.DragQueryPoint(dropHandle, out NativePoint dropPoint); + FileDropEvent?.Invoke(files, new PointInt32(dropPoint.X, dropPoint.Y)); + } + finally + { + NativeFileDrop.DragFinish(dropHandle); + } + } + + private static unsafe partial class NativeFileDrop + { + [LibraryImport("shell32.dll")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + internal static partial void DragAcceptFiles(nint windowHandle, [MarshalAs(UnmanagedType.Bool)] bool acceptFiles); + + [LibraryImport("shell32.dll", EntryPoint = "DragQueryFileW")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + internal static partial uint DragQueryFile(nint dropHandle, uint fileIndex, char* filePath, uint filePathLength); + + [LibraryImport("shell32.dll")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool DragQueryPoint(nint dropHandle, out NativePoint dropPoint); + + [LibraryImport("shell32.dll")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + internal static partial void DragFinish(nint dropHandle); + + [LibraryImport("user32.dll")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool ChangeWindowMessageFilterEx(nint windowHandle, uint message, uint action, nint changeFilterStruct); + + [LibraryImport("user32.dll")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + internal static partial short GetAsyncKeyState(int virtualKey); + + [LibraryImport("user32.dll")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool GetCursorPos(out NativePoint cursorPosition); + + [LibraryImport("user32.dll")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool ScreenToClient(nint windowHandle, ref NativePoint cursorPosition); + } + + [StructLayout(LayoutKind.Sequential)] + private struct NativePoint + { + internal int X; + internal int Y; + } + #endregion #region Titlebar Methods diff --git a/CollapseLauncher/Classes/Plugins/PluginImporter.cs b/CollapseLauncher/Classes/Plugins/PluginImporter.cs index b8f3b496b..50b908ca8 100644 --- a/CollapseLauncher/Classes/Plugins/PluginImporter.cs +++ b/CollapseLauncher/Classes/Plugins/PluginImporter.cs @@ -1,5 +1,6 @@ using Hi3Helper.Plugin.Core.Update; using Hi3Helper.Shared.Region; +using CollapseLauncher.Helper.StreamUtility; using System; using System.Data; using System.IO; @@ -15,6 +16,14 @@ internal static partial class PluginImporter public static async Task AutoGetImportFromPath(string filePath, CancellationToken token) { bool isFromPackage = filePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase); + bool isFromManifest = string.Equals(Path.GetFileName(filePath), + PluginManager.ManifestPrefix, + StringComparison.OrdinalIgnoreCase); + + if (!isFromPackage && !isFromManifest) + { + throw new NotSupportedException($"{Path.GetFileName(filePath)} is not supported. Import a .zip package or a file named manifest.json."); + } using IPluginSource pluginSource = isFromPackage ? await ZipPluginSource.GetSourceFrom(filePath, token) : @@ -24,6 +33,7 @@ await ZipPluginSource.GetSourceFrom(filePath, token) : string pluginDirName = $"Hi3Helper.Plugin.{pluginExecNameNoExt}"; string pluginBaseDir = Path.Combine(LauncherConfig.AppPluginFolder, pluginDirName); string pluginRelName = Path.Combine(pluginDirName, pluginSource.Manifest.MainLibraryName); + string pluginEntryPath = GetContainedPath(pluginBaseDir, pluginSource.Manifest.MainLibraryName); if (PluginManager.PluginInstances.ContainsKey(pluginBaseDir) || PluginManager.PluginInstances.Values @@ -44,25 +54,64 @@ await ZipPluginSource.GetSourceFrom(filePath, token) : }); } - string pluginEntryPath = Path.Combine(pluginBaseDir, pluginSource.Manifest.MainLibraryName); - foreach (PluginManifestAssetInfo asset in pluginSource.Manifest.Assets) + Directory.CreateDirectory(LauncherConfig.AppPluginFolder); + string stagingDir = Path.Combine(LauncherConfig.AppPluginFolder, $".import-{Guid.NewGuid():N}"); + string cleanupDir = stagingDir; + + try { - string assetFullPath = Path.Combine(pluginBaseDir, asset.FilePath); - string? assetFullDir = Path.GetDirectoryName(assetFullPath); + foreach (PluginManifestAssetInfo asset in pluginSource.Manifest.Assets) + { + string assetFullPath = GetContainedPath(stagingDir, asset.FilePath); + string? assetFullDir = Path.GetDirectoryName(assetFullPath); + + if (!string.IsNullOrEmpty(assetFullDir)) + { + Directory.CreateDirectory(assetFullDir); + } + + await using FileStream assetStream = File.Create(assetFullPath); + await using Stream assetPluginSourceStream = await pluginSource.GetAssetStream(asset, token); + + await assetPluginSourceStream.CopyToAsync(assetStream, token); + } + + Directory.Move(stagingDir, pluginBaseDir); + cleanupDir = pluginBaseDir; - if (!string.IsNullOrEmpty(assetFullDir)) + PluginInfo pluginInfo = new(pluginEntryPath, + pluginRelName, + pluginSource.Manifest); + cleanupDir = string.Empty; + return pluginInfo; + } + catch + { + if (!string.IsNullOrEmpty(cleanupDir)) { - Directory.CreateDirectory(assetFullDir); + new DirectoryInfo(cleanupDir).TryDeleteDirectory(true); } - await using FileStream assetStream = File.Create(assetFullPath); - await using Stream assetPluginSourceStream = await pluginSource.GetAssetStream(asset, token); + throw; + } + } + + private static string GetContainedPath(string baseDirectory, string relativePath) + { + if (string.IsNullOrWhiteSpace(relativePath)) + { + throw new InvalidDataException("The plugin manifest contains an empty file path."); + } - await assetPluginSourceStream.CopyToAsync(assetStream, token); + string baseFullPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(baseDirectory)); + string fileFullPath = Path.GetFullPath(relativePath, baseFullPath); + string basePrefix = baseFullPath + Path.DirectorySeparatorChar; + + if (!fileFullPath.StartsWith(basePrefix, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException($"The plugin manifest path '{relativePath}' points outside the plugin directory."); } - return new PluginInfo(pluginEntryPath, - pluginRelName, - pluginSource.Manifest); + return fileFullPath; } } diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/PluginManagerPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/PluginManagerPage.xaml index 249ce132e..d3e2c4773 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/PluginManagerPage.xaml +++ b/CollapseLauncher/XAMLs/MainApp/Pages/PluginManagerPage.xaml @@ -823,12 +823,17 @@ + + + + + + +