Windows kept its existing IFileDialog implementation; macOS uses osascript, Linux tries zenity then falls back to kdialog. Fails open (empty string, no crash) if no picker is available, matching the existing Windows behavior.
211 lines
7.9 KiB
Python
211 lines
7.9 KiB
Python
WEB_DIRECTORY = "./js"
|
|
|
|
from .smart_prompt_controller import NODE_CLASS_MAPPINGS as A, NODE_DISPLAY_NAME_MAPPINGS as A_NAMES
|
|
from .smart_profile_switch import NODE_CLASS_MAPPINGS as B, NODE_DISPLAY_NAME_MAPPINGS as B_NAMES
|
|
from .smart_resize import NODE_CLASS_MAPPINGS as C, NODE_DISPLAY_NAME_MAPPINGS as C_NAMES
|
|
from .smart_batch_resize import NODE_CLASS_MAPPINGS as D, NODE_DISPLAY_NAME_MAPPINGS as D_NAMES
|
|
from .smart_resolution_multiplier import NODE_CLASS_MAPPINGS as E, NODE_DISPLAY_NAME_MAPPINGS as E_NAMES
|
|
|
|
NODE_CLASS_MAPPINGS = {**A, **B, **C, **D, **E}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {**A_NAMES, **B_NAMES, **C_NAMES, **D_NAMES, **E_NAMES}
|
|
|
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
|
|
|
try:
|
|
import asyncio
|
|
import concurrent.futures
|
|
import os
|
|
import threading
|
|
from aiohttp import web
|
|
from server import PromptServer
|
|
|
|
_dialog_lock = threading.Lock()
|
|
# Must match the literal 'CRAFTKIT_DIALOG_ERROR:' prefix written by the
|
|
# PowerShell catch block below (kept literal there to avoid brace-escaping
|
|
# a dense here-string).
|
|
_DIALOG_ERROR_PREFIX = "CRAFTKIT_DIALOG_ERROR:"
|
|
|
|
def _open_dialog_macos():
|
|
import subprocess
|
|
script = 'POSIX path of (choose folder with prompt "Select input folder")'
|
|
try:
|
|
result = subprocess.run(
|
|
["osascript", "-e", script],
|
|
capture_output=True, text=True, timeout=300
|
|
)
|
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
return ""
|
|
if result.returncode != 0:
|
|
return "" # cancelled, or osascript unavailable
|
|
return result.stdout.strip()
|
|
|
|
_LINUX_PICKER_COMMANDS = [
|
|
["zenity", "--file-selection", "--directory", "--title=Select input folder"],
|
|
["kdialog", "--getexistingdirectory", os.path.expanduser("~"), "--title", "Select input folder"],
|
|
]
|
|
|
|
def _open_dialog_linux():
|
|
import subprocess
|
|
for cmd in _LINUX_PICKER_COMMANDS:
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
except FileNotFoundError:
|
|
continue
|
|
except subprocess.TimeoutExpired:
|
|
return ""
|
|
if result.returncode != 0:
|
|
return "" # cancelled
|
|
return result.stdout.strip()
|
|
print("[CraftKit] No folder picker found (tried zenity, kdialog); paste input folder paths manually.")
|
|
return ""
|
|
|
|
def _open_dialog_windows():
|
|
import subprocess
|
|
ps = r"""
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
Add-Type @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
public static class CraftKitFolderPicker
|
|
{
|
|
[ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
|
|
private class FileOpenDialogRCW { }
|
|
|
|
[ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
private interface IFileDialog
|
|
{
|
|
[PreserveSig] int Show(IntPtr parent);
|
|
void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
|
|
void SetFileTypeIndex(uint iFileType);
|
|
void GetFileTypeIndex(out uint piFileType);
|
|
void Advise(IntPtr pfde, out uint pdwCookie);
|
|
void Unadvise(uint dwCookie);
|
|
void SetOptions(uint fos);
|
|
void GetOptions(out uint fos);
|
|
void SetDefaultFolder(IntPtr psi);
|
|
void SetFolder(IntPtr psi);
|
|
void GetFolder(out IntPtr ppsi);
|
|
void GetCurrentSelection(out IntPtr ppsi);
|
|
void SetFileName(string pszName);
|
|
void GetFileName(out IntPtr pszName);
|
|
void SetTitle(string pszTitle);
|
|
void SetOkButtonLabel(string pszText);
|
|
void SetFileNameLabel(string pszLabel);
|
|
void GetResult(out IShellItemLocal ppsi);
|
|
void AddPlace(IntPtr psi, uint fdap);
|
|
void SetDefaultExtension(string pszDefaultExtension);
|
|
void Close(int hr);
|
|
void SetClientGuid(ref Guid guid);
|
|
void ClearClientData();
|
|
void SetFilter(IntPtr pFilter);
|
|
}
|
|
|
|
[ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
private interface IShellItemLocal
|
|
{
|
|
void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
|
|
void GetParent(out IShellItemLocal ppsi);
|
|
void GetDisplayName(uint sigdnName, out IntPtr ppszName);
|
|
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
|
void Compare(IShellItemLocal psi, uint hint, out int piOrder);
|
|
}
|
|
|
|
public static string ShowDialog(IntPtr owner, string title)
|
|
{
|
|
const uint ERROR_CANCELLED = 0x800704C7;
|
|
var dialog = (IFileDialog)new FileOpenDialogRCW();
|
|
dialog.SetOptions(0x20u | 0x40u); // FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM
|
|
if (!string.IsNullOrEmpty(title)) dialog.SetTitle(title);
|
|
int hr = dialog.Show(owner);
|
|
if (hr != 0)
|
|
{
|
|
if ((uint)hr == ERROR_CANCELLED) return null;
|
|
throw new System.Runtime.InteropServices.COMException("IFileDialog.Show failed", hr);
|
|
}
|
|
IShellItemLocal item;
|
|
dialog.GetResult(out item);
|
|
IntPtr pszPath = IntPtr.Zero;
|
|
try
|
|
{
|
|
item.GetDisplayName(0x80058000u, out pszPath); // SIGDN_FILESYSPATH
|
|
return Marshal.PtrToStringUni(pszPath);
|
|
}
|
|
finally
|
|
{
|
|
if (pszPath != IntPtr.Zero) Marshal.FreeCoTaskMem(pszPath);
|
|
}
|
|
}
|
|
}
|
|
"@
|
|
|
|
$r = ''
|
|
$o = New-Object System.Windows.Forms.Form
|
|
$o.TopMost = $true
|
|
$o.ShowInTaskbar = $false
|
|
$o.FormBorderStyle = 'None'
|
|
$o.Width = 1; $o.Height = 1; $o.Opacity = 0
|
|
$o.StartPosition = 'CenterScreen'
|
|
$o.Add_Shown({
|
|
$o.Activate()
|
|
try {
|
|
$path = [CraftKitFolderPicker]::ShowDialog($o.Handle, 'Select input folder')
|
|
if ($path) { $script:r = $path }
|
|
} catch {
|
|
# Must match _DIALOG_ERROR_PREFIX in __init__.py.
|
|
$script:r = 'CRAFTKIT_DIALOG_ERROR:' + $_.Exception.Message
|
|
}
|
|
$o.Close()
|
|
})
|
|
$o.ShowDialog() | Out-Null
|
|
$r
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
["powershell", "-NoProfile", "-Command", ps],
|
|
capture_output=True, text=True, timeout=300
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return ""
|
|
return result.stdout.strip()
|
|
|
|
def _open_dialog():
|
|
import sys
|
|
if sys.platform == "win32":
|
|
return _open_dialog_windows()
|
|
elif sys.platform == "darwin":
|
|
return _open_dialog_macos()
|
|
else:
|
|
return _open_dialog_linux()
|
|
|
|
async def browse_folder(request):
|
|
if request.remote not in ("127.0.0.1", "::1"):
|
|
return web.json_response({"ok": False, "error": "forbidden"}, status=403)
|
|
|
|
origin = request.headers.get("Origin")
|
|
if origin is not None:
|
|
host = request.headers.get("Host", "")
|
|
origin_host = origin.split("://", 1)[-1]
|
|
if origin_host != host:
|
|
return web.json_response({"ok": False, "error": "forbidden"}, status=403)
|
|
|
|
if not _dialog_lock.acquire(blocking=False):
|
|
return web.json_response({"ok": False, "error": "A folder dialog is already open."}, status=409)
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
folder = await loop.run_in_executor(pool, _open_dialog)
|
|
finally:
|
|
_dialog_lock.release()
|
|
|
|
if folder.startswith(_DIALOG_ERROR_PREFIX):
|
|
return web.json_response({"ok": False, "error": folder.removeprefix(_DIALOG_ERROR_PREFIX)}, status=500)
|
|
|
|
if folder and os.path.isdir(folder):
|
|
return web.json_response({"ok": True, "path": folder})
|
|
return web.json_response({"ok": False, "cancelled": True})
|
|
|
|
PromptServer.instance.routes.post("/craftkit/browse_folder")(browse_folder)
|
|
except Exception as e:
|
|
print(f"[CraftKit] Browse folder endpoint unavailable ({e}); paste input folder paths manually.")
|