From ffe0670a275996df5d6e97d1becca602dff81df5 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 27 Apr 2026 14:05:21 +0800 Subject: [PATCH] feat(example-images): add remote open mode support --- locales/de.json | 18 +++ locales/en.json | 18 +++ locales/es.json | 18 +++ locales/fr.json | 18 +++ locales/he.json | 18 +++ locales/ja.json | 18 +++ locales/ko.json | 18 +++ locales/ru.json | 18 +++ locales/zh-CN.json | 18 +++ locales/zh-TW.json | 18 +++ py/services/settings_manager.py | 3 + py/utils/example_images_file_manager.py | 122 +++++++++++++++--- settings.json.example | 3 + static/js/managers/SettingsManager.js | 36 ++++++ static/js/state/index.js | 3 + static/js/utils/uiHelpers.js | 53 +++++++- .../components/modals/settings_modal.html | 56 +++++++- .../managers/settingsManager.library.test.js | 48 +++++++ tests/frontend/utils/uiHelpers.dom.test.js | 47 +++++++ .../utils/test_example_images_file_manager.py | 91 +++++++++++++ 20 files changed, 621 insertions(+), 21 deletions(-) diff --git a/locales/de.json b/locales/de.json index d57ca9b7..89d7b389 100644 --- a/locales/de.json +++ b/locales/de.json @@ -538,6 +538,20 @@ "downloadLocationHelp": "Geben Sie den Ordnerpfad ein, wo Beispielbilder von Civitai gespeichert werden", "autoDownload": "Beispielbilder automatisch herunterladen", "autoDownloadHelp": "Beispielbilder automatisch für Modelle herunterladen, die keine haben (erfordert gesetzten Download-Speicherort)", + "openMode": "Aktion für Beispielbilder öffnen", + "openModeHelp": "Wählen Sie, ob die Aktion auf dem Server geöffnet, ein zugeordneter lokaler Pfad kopiert oder eine benutzerdefinierte URI gestartet werden soll.", + "openModeOptions": { + "system": "Auf Server öffnen", + "clipboard": "Lokalen Pfad kopieren", + "uriTemplate": "Benutzerdefinierte URI öffnen" + }, + "localRoot": "Lokales Stammverzeichnis für Beispielbilder", + "localRootHelp": "Optionales lokales oder eingebundenes Stammverzeichnis, das das Beispielbild-Verzeichnis des Servers widerspiegelt. Wenn leer, wird der Serverpfad wiederverwendet.", + "localRootPlaceholder": "Beispiel: /Volumes/ComfyUI/example_images", + "uriTemplate": "URI-Vorlage öffnen", + "uriTemplateHelp": "Verwenden Sie einen benutzerdefinierten Deeplink wie eine Datei-URI oder einen Shortcuts-Link.", + "uriTemplatePlaceholder": "Beispiel: shortcuts://run-shortcut?name=Open%20Finder&input=text&text={{encoded_local_path}}", + "uriTemplatePlaceholders": "Verfügbare Platzhalter: {{local_path}}, {{encoded_local_path}}, {{relative_path}}, {{encoded_relative_path}}, {{file_uri}}, {{encoded_file_uri}}", "optimizeImages": "Heruntergeladene Bilder optimieren", "optimizeImagesHelp": "Beispielbilder optimieren, um Dateigröße zu reduzieren und Ladegeschwindigkeit zu verbessern (Metadaten bleiben erhalten)", "download": "Herunterladen", @@ -1442,6 +1456,10 @@ "opened": "Beispielbilder-Ordner geöffnet", "openingFolder": "Beispielbilder-Ordner wird geöffnet", "failedToOpen": "Fehler beim Öffnen des Beispielbilder-Ordners", + "copiedPath": "Pfad in Zwischenablage kopiert: {{path}}", + "clipboardFallback": "Pfad: {{path}}", + "copiedUri": "Link in Zwischenablage kopiert: {{uri}}", + "uriClipboardFallback": "Link: {{uri}}", "setupRequired": "Beispielbilder-Speicher", "setupDescription": "Um benutzerdefinierte Beispielbilder hinzuzufügen, müssen Sie zuerst einen Download-Speicherort festlegen.", "setupUsage": "Dieser Pfad wird sowohl für heruntergeladene als auch für benutzerdefinierte Beispielbilder verwendet.", diff --git a/locales/en.json b/locales/en.json index d269b56b..16ccd9fc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -538,6 +538,20 @@ "downloadLocationHelp": "Enter the folder path where example images from Civitai will be saved", "autoDownload": "Auto Download Example Images", "autoDownloadHelp": "Automatically download example images for models that don't have them (requires download location to be set)", + "openMode": "Open Example Images Action", + "openModeHelp": "Choose whether the action opens on the server, copies a mapped local path, or launches a custom URI.", + "openModeOptions": { + "system": "Open on server", + "clipboard": "Copy local path", + "uriTemplate": "Open custom URI" + }, + "localRoot": "Local Example Images Root", + "localRootHelp": "Optional local or mounted root that mirrors the server example images directory. If blank, the server path is reused.", + "localRootPlaceholder": "Example: /Volumes/ComfyUI/example_images", + "uriTemplate": "Open URI Template", + "uriTemplateHelp": "Use a custom deep link such as a file URI or a Shortcuts link.", + "uriTemplatePlaceholder": "Example: shortcuts://run-shortcut?name=Open%20Finder&input=text&text={{encoded_local_path}}", + "uriTemplatePlaceholders": "Available placeholders: {{local_path}}, {{encoded_local_path}}, {{relative_path}}, {{encoded_relative_path}}, {{file_uri}}, {{encoded_file_uri}}", "optimizeImages": "Optimize Downloaded Images", "optimizeImagesHelp": "Optimize example images to reduce file size and improve loading speed (metadata will be preserved)", "download": "Download", @@ -1442,6 +1456,10 @@ "opened": "Example images folder opened", "openingFolder": "Opening example images folder", "failedToOpen": "Failed to open example images folder", + "copiedPath": "Path copied to clipboard: {{path}}", + "clipboardFallback": "Path: {{path}}", + "copiedUri": "Link copied to clipboard: {{uri}}", + "uriClipboardFallback": "Link: {{uri}}", "setupRequired": "Example Images Storage", "setupDescription": "To add custom example images, you need to set a download location first.", "setupUsage": "This path is used for both downloaded and custom example images.", diff --git a/locales/es.json b/locales/es.json index 8e3fa5d7..77ff2b3b 100644 --- a/locales/es.json +++ b/locales/es.json @@ -538,6 +538,20 @@ "downloadLocationHelp": "Introduce la ruta de la carpeta donde se guardarán las imágenes de ejemplo de Civitai", "autoDownload": "Descargar automáticamente imágenes de ejemplo", "autoDownloadHelp": "Descargar automáticamente imágenes de ejemplo para modelos que no las tengan (requiere que se establezca la ubicación de descarga)", + "openMode": "Acción al abrir imágenes de ejemplo", + "openModeHelp": "Elige si la acción se abre en el servidor, copia una ruta local asignada o lanza una URI personalizada.", + "openModeOptions": { + "system": "Abrir en el servidor", + "clipboard": "Copiar ruta local", + "uriTemplate": "Abrir URI personalizada" + }, + "localRoot": "Raíz local de imágenes de ejemplo", + "localRootHelp": "Raíz local u montada opcional que refleja el directorio de imágenes de ejemplo del servidor. Si se deja en blanco, se reutiliza la ruta del servidor.", + "localRootPlaceholder": "Ejemplo: /Volumes/ComfyUI/example_images", + "uriTemplate": "Abrir plantilla de URI", + "uriTemplateHelp": "Usa un enlace profundo personalizado, como un URI de archivo o un enlace de Shortcuts.", + "uriTemplatePlaceholder": "Ejemplo: shortcuts://run-shortcut?name=Open%20Finder&input=text&text={{encoded_local_path}}", + "uriTemplatePlaceholders": "Marcadores disponibles: {{local_path}}, {{encoded_local_path}}, {{relative_path}}, {{encoded_relative_path}}, {{file_uri}}, {{encoded_file_uri}}", "optimizeImages": "Optimizar imágenes descargadas", "optimizeImagesHelp": "Optimizar imágenes de ejemplo para reducir el tamaño del archivo y mejorar la velocidad de carga (se preservarán los metadatos)", "download": "Descargar", @@ -1442,6 +1456,10 @@ "opened": "Carpeta de imágenes de ejemplo abierta", "openingFolder": "Abriendo carpeta de imágenes de ejemplo", "failedToOpen": "Error al abrir carpeta de imágenes de ejemplo", + "copiedPath": "Ruta copiada al portapapeles: {{path}}", + "clipboardFallback": "Ruta: {{path}}", + "copiedUri": "Enlace copiado al portapapeles: {{uri}}", + "uriClipboardFallback": "Enlace: {{uri}}", "setupRequired": "Almacenamiento de imágenes de ejemplo", "setupDescription": "Para agregar imágenes de ejemplo personalizadas, primero necesita establecer una ubicación de descarga.", "setupUsage": "Esta ruta se utiliza tanto para imágenes de ejemplo descargadas como personalizadas.", diff --git a/locales/fr.json b/locales/fr.json index b061fe9c..ac5e48f5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -538,6 +538,20 @@ "downloadLocationHelp": "Entrez le chemin du dossier où les images d'exemple de Civitai seront sauvegardées", "autoDownload": "Téléchargement automatique des images d'exemple", "autoDownloadHelp": "Télécharger automatiquement les images d'exemple pour les modèles qui n'en ont pas (nécessite que l'emplacement de téléchargement soit défini)", + "openMode": "Action d’ouverture des images d’exemple", + "openModeHelp": "Choisissez si l’action s’ouvre sur le serveur, copie un chemin local mappé ou lance une URI personnalisée.", + "openModeOptions": { + "system": "Ouvrir sur le serveur", + "clipboard": "Copier le chemin local", + "uriTemplate": "Ouvrir une URI personnalisée" + }, + "localRoot": "Racine locale des images d’exemple", + "localRootHelp": "Racine locale ou montée facultative qui reflète le répertoire des images d’exemple du serveur. Si vide, le chemin du serveur est réutilisé.", + "localRootPlaceholder": "Exemple : /Volumes/ComfyUI/example_images", + "uriTemplate": "Ouvrir le modèle d’URI", + "uriTemplateHelp": "Utilisez un lien profond personnalisé, tel qu’une URI de fichier ou un lien Shortcuts.", + "uriTemplatePlaceholder": "Exemple : shortcuts://run-shortcut?name=Open%20Finder&input=text&text={{encoded_local_path}}", + "uriTemplatePlaceholders": "Paramètres disponibles : {{local_path}}, {{encoded_local_path}}, {{relative_path}}, {{encoded_relative_path}}, {{file_uri}}, {{encoded_file_uri}}", "optimizeImages": "Optimiser les images téléchargées", "optimizeImagesHelp": "Optimiser les images d'exemple pour réduire la taille du fichier et améliorer la vitesse de chargement (les métadonnées seront préservées)", "download": "Télécharger", @@ -1442,6 +1456,10 @@ "opened": "Dossier d'images d'exemple ouvert", "openingFolder": "Ouverture du dossier d'images d'exemple", "failedToOpen": "Échec de l'ouverture du dossier d'images d'exemple", + "copiedPath": "Chemin copié dans le presse-papiers : {{path}}", + "clipboardFallback": "Chemin : {{path}}", + "copiedUri": "Lien copié dans le presse-papiers : {{uri}}", + "uriClipboardFallback": "Lien : {{uri}}", "setupRequired": "Stockage d'images d'exemple", "setupDescription": "Pour ajouter des images d'exemple personnalisées, vous devez d'abord définir un emplacement de téléchargement.", "setupUsage": "Ce chemin est utilisé pour les images d'exemple téléchargées et personnalisées.", diff --git a/locales/he.json b/locales/he.json index 68887408..37ab77b3 100644 --- a/locales/he.json +++ b/locales/he.json @@ -538,6 +538,20 @@ "downloadLocationHelp": "הזן את נתיב התיקייה שבו יישמרו תמונות דוגמה מ-Civitai", "autoDownload": "הורדה אוטומטית של תמונות דוגמה", "autoDownloadHelp": "הורד אוטומטית תמונות דוגמה למודלים שאין להם (דורש הגדרת מיקום הורדה)", + "openMode": "פעולת פתיחת תמונות דוגמה", + "openModeHelp": "בחר אם הפעולה תיפתח בשרת, תעתיק נתיב מקומי ממופה או תפעיל URI מותאם אישית.", + "openModeOptions": { + "system": "פתח בשרת", + "clipboard": "העתק נתיב מקומי", + "uriTemplate": "פתח URI מותאם אישית" + }, + "localRoot": "שורש מקומי לתמונות דוגמה", + "localRootHelp": "שורש מקומי או ממופה אופציונלי שמשקף את תיקיית תמונות הדוגמה בשרת. אם השדה ריק, ייעשה שימוש חוזר בנתיב השרת.", + "localRootPlaceholder": "דוגמה: /Volumes/ComfyUI/example_images", + "uriTemplate": "תבנית URI לפתיחה", + "uriTemplateHelp": "השתמש בקישור עומק מותאם אישית כמו URI של קובץ או קישור Shortcuts.", + "uriTemplatePlaceholder": "דוגמה: shortcuts://run-shortcut?name=Open%20Finder&input=text&text={{encoded_local_path}}", + "uriTemplatePlaceholders": "מצייני מקום זמינים: {{local_path}}, {{encoded_local_path}}, {{relative_path}}, {{encoded_relative_path}}, {{file_uri}}, {{encoded_file_uri}}", "optimizeImages": "מטב תמונות שהורדו", "optimizeImagesHelp": "מטב תמונות דוגמה כדי להקטין את גודל הקובץ ולשפר את מהירות הטעינה (מטא-דאטה תישמר)", "download": "הורד", @@ -1442,6 +1456,10 @@ "opened": "תיקיית תמונות הדוגמה נפתחה", "openingFolder": "פותח תיקיית תמונות דוגמה", "failedToOpen": "פתיחת תיקיית תמונות הדוגמה נכשלה", + "copiedPath": "הנתיב הועתק ללוח: {{path}}", + "clipboardFallback": "נתיב: {{path}}", + "copiedUri": "הקישור הועתק ללוח: {{uri}}", + "uriClipboardFallback": "קישור: {{uri}}", "setupRequired": "אחסון תמונות דוגמה", "setupDescription": "כדי להוסיף תמונות דוגמה מותאמות אישית, עליך קודם להגדיר מיקום הורדה.", "setupUsage": "נתיב זה משמש הן עבור תמונות דוגמה שהורדו והן עבור תמונות מותאמות אישית.", diff --git a/locales/ja.json b/locales/ja.json index 3e712f2c..a4422f75 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -538,6 +538,20 @@ "downloadLocationHelp": "Civitaiからの例画像を保存するフォルダパスを入力してください", "autoDownload": "例画像の自動ダウンロード", "autoDownloadHelp": "例画像がないモデルの例画像を自動的にダウンロードします(ダウンロード場所の設定が必要)", + "openMode": "サンプル画像を開く動作", + "openModeHelp": "サーバー上で開くか、対応するローカルパスをコピーするか、カスタム URI を起動するかを選択します。", + "openModeOptions": { + "system": "サーバー上で開く", + "clipboard": "ローカルパスをコピー", + "uriTemplate": "カスタム URI を開く" + }, + "localRoot": "ローカルのサンプル画像ルート", + "localRootHelp": "サーバーのサンプル画像ディレクトリを反映する任意のローカルまたはマウント済みルートです。空欄の場合はサーバーのパスを再利用します。", + "localRootPlaceholder": "例: /Volumes/ComfyUI/example_images", + "uriTemplate": "URI テンプレートを開く", + "uriTemplateHelp": "ファイル URI や Shortcuts リンクなどのカスタムディープリンクを使用します。", + "uriTemplatePlaceholder": "例: shortcuts://run-shortcut?name=Open%20Finder&input=text&text={{encoded_local_path}}", + "uriTemplatePlaceholders": "使用可能なプレースホルダー: {{local_path}}, {{encoded_local_path}}, {{relative_path}}, {{encoded_relative_path}}, {{file_uri}}, {{encoded_file_uri}}", "optimizeImages": "ダウンロード画像の最適化", "optimizeImagesHelp": "例画像を最適化してファイルサイズを縮小し、読み込み速度を向上させます(メタデータは保持されます)", "download": "ダウンロード", @@ -1442,6 +1456,10 @@ "opened": "例画像フォルダが開かれました", "openingFolder": "例画像フォルダを開いています", "failedToOpen": "例画像フォルダを開くのに失敗しました", + "copiedPath": "パスをクリップボードにコピーしました: {{path}}", + "clipboardFallback": "パス: {{path}}", + "copiedUri": "リンクをクリップボードにコピーしました: {{uri}}", + "uriClipboardFallback": "リンク: {{uri}}", "setupRequired": "例画像ストレージ", "setupDescription": "カスタム例画像を追加するには、まずダウンロード場所を設定する必要があります。", "setupUsage": "このパスは、ダウンロードした例画像とカスタム画像の両方に使用されます。", diff --git a/locales/ko.json b/locales/ko.json index 0b930621..d64ec7b4 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -538,6 +538,20 @@ "downloadLocationHelp": "Civitai의 예시 이미지가 저장될 폴더 경로를 입력하세요", "autoDownload": "예시 이미지 자동 다운로드", "autoDownloadHelp": "예시 이미지가 없는 모델의 예시 이미지를 자동으로 다운로드합니다 (다운로드 위치 설정 필요)", + "openMode": "예시 이미지 열기 동작", + "openModeHelp": "서버에서 열지, 매핑된 로컬 경로를 복사할지, 사용자 지정 URI를 실행할지 선택합니다.", + "openModeOptions": { + "system": "서버에서 열기", + "clipboard": "로컬 경로 복사", + "uriTemplate": "사용자 지정 URI 열기" + }, + "localRoot": "로컬 예시 이미지 루트", + "localRootHelp": "서버 예시 이미지 디렉터리를 반영하는 선택적 로컬 또는 마운트된 루트입니다. 비워 두면 서버 경로를 재사용합니다.", + "localRootPlaceholder": "예: /Volumes/ComfyUI/example_images", + "uriTemplate": "URI 템플릿 열기", + "uriTemplateHelp": "파일 URI 또는 Shortcuts 링크 같은 사용자 지정 딥링크를 사용합니다.", + "uriTemplatePlaceholder": "예: shortcuts://run-shortcut?name=Open%20Finder&input=text&text={{encoded_local_path}}", + "uriTemplatePlaceholders": "사용 가능한 플레이스홀더: {{local_path}}, {{encoded_local_path}}, {{relative_path}}, {{encoded_relative_path}}, {{file_uri}}, {{encoded_file_uri}}", "optimizeImages": "다운로드된 이미지 최적화", "optimizeImagesHelp": "파일 크기를 줄이고 로딩 속도를 향상시키기 위해 예시 이미지를 최적화합니다 (메타데이터는 보존됨)", "download": "다운로드", @@ -1442,6 +1456,10 @@ "opened": "예시 이미지 폴더가 열렸습니다", "openingFolder": "예시 이미지 폴더를 여는 중", "failedToOpen": "예시 이미지 폴더 열기 실패", + "copiedPath": "경로를 클립보드에 복사했습니다: {{path}}", + "clipboardFallback": "경로: {{path}}", + "copiedUri": "링크를 클립보드에 복사했습니다: {{uri}}", + "uriClipboardFallback": "링크: {{uri}}", "setupRequired": "예시 이미지 저장소", "setupDescription": "사용자 지정 예시 이미지를 추가하려면 먼저 다운로드 위치를 설정해야 합니다.", "setupUsage": "이 경로는 다운로드한 예시 이미지와 사용자 지정 이미지 모두에 사용됩니다.", diff --git a/locales/ru.json b/locales/ru.json index cd6268ac..d18c215a 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -538,6 +538,20 @@ "downloadLocationHelp": "Введите путь к папке, где будут сохраняться примеры изображений с Civitai", "autoDownload": "Автозагрузка примеров изображений", "autoDownloadHelp": "Автоматически загружать примеры изображений для моделей, у которых их нет (требует настройки места загрузки)", + "openMode": "Действие открытия примеров изображений", + "openModeHelp": "Выберите, будет ли действие открывать папку на сервере, копировать сопоставленный локальный путь или запускать пользовательский URI.", + "openModeOptions": { + "system": "Открыть на сервере", + "clipboard": "Скопировать локальный путь", + "uriTemplate": "Открыть пользовательский URI" + }, + "localRoot": "Локальный корень примеров изображений", + "localRootHelp": "Необязательный локальный или смонтированный корневой путь, отражающий каталог примеров изображений на сервере. Если оставить пустым, будет использован путь сервера.", + "localRootPlaceholder": "Пример: /Volumes/ComfyUI/example_images", + "uriTemplate": "Шаблон URI для открытия", + "uriTemplateHelp": "Используйте пользовательскую deep link-ссылку, например file URI или ссылку Shortcuts.", + "uriTemplatePlaceholder": "Пример: shortcuts://run-shortcut?name=Open%20Finder&input=text&text={{encoded_local_path}}", + "uriTemplatePlaceholders": "Доступные плейсхолдеры: {{local_path}}, {{encoded_local_path}}, {{relative_path}}, {{encoded_relative_path}}, {{file_uri}}, {{encoded_file_uri}}", "optimizeImages": "Оптимизировать загруженные изображения", "optimizeImagesHelp": "Оптимизировать примеры изображений для уменьшения размера файла и улучшения скорости загрузки (метаданные будут сохранены)", "download": "Загрузить", @@ -1442,6 +1456,10 @@ "opened": "Папка с примерами изображений открыта", "openingFolder": "Открытие папки с примерами изображений", "failedToOpen": "Не удалось открыть папку с примерами изображений", + "copiedPath": "Путь скопирован в буфер обмена: {{path}}", + "clipboardFallback": "Путь: {{path}}", + "copiedUri": "Ссылка скопирована в буфер обмена: {{uri}}", + "uriClipboardFallback": "Ссылка: {{uri}}", "setupRequired": "Хранилище примеров изображений", "setupDescription": "Чтобы добавить собственные примеры изображений, сначала нужно установить место загрузки.", "setupUsage": "Этот путь используется как для загруженных, так и для пользовательских примеров изображений.", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 4750a73a..f046b464 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -538,6 +538,20 @@ "downloadLocationHelp": "输入保存从 Civitai 下载的示例图片的文件夹路径", "autoDownload": "自动下载示例图片", "autoDownloadHelp": "自动为没有示例图片的模型下载示例图片(需设置下载位置)", + "openMode": "打开示例图片操作", + "openModeHelp": "选择是在服务器上打开、复制映射后的本地路径,还是启动自定义 URI。", + "openModeOptions": { + "system": "在服务器上打开", + "clipboard": "复制本地路径", + "uriTemplate": "打开自定义 URI" + }, + "localRoot": "本地示例图片根目录", + "localRootHelp": "可选的本地或挂载根目录,用于映射服务器上的示例图片目录。若留空,则复用服务器路径。", + "localRootPlaceholder": "例如:/Volumes/ComfyUI/example_images", + "uriTemplate": "打开 URI 模板", + "uriTemplateHelp": "使用自定义深链接,例如文件 URI 或 Shortcuts 链接。", + "uriTemplatePlaceholder": "例如:shortcuts://run-shortcut?name=Open%20Finder&input=text&text={{encoded_local_path}}", + "uriTemplatePlaceholders": "可用占位符:{{local_path}}、{{encoded_local_path}}、{{relative_path}}、{{encoded_relative_path}}、{{file_uri}}、{{encoded_file_uri}}", "optimizeImages": "优化下载图片", "optimizeImagesHelp": "优化示例图片以减少文件大小并提升加载速度(保留元数据)", "download": "下载", @@ -1442,6 +1456,10 @@ "opened": "示例图片文件夹已打开", "openingFolder": "正在打开示例图片文件夹", "failedToOpen": "打开示例图片文件夹失败", + "copiedPath": "路径已复制到剪贴板:{{path}}", + "clipboardFallback": "路径:{{path}}", + "copiedUri": "链接已复制到剪贴板:{{uri}}", + "uriClipboardFallback": "链接:{{uri}}", "setupRequired": "示例图片存储", "setupDescription": "要添加自定义示例图片,您需要先设置下载位置。", "setupUsage": "此路径用于存储下载的示例图片和自定义图片。", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 84755166..c3c485a7 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -538,6 +538,20 @@ "downloadLocationHelp": "輸入從 Civitai 下載範例圖片要儲存的資料夾路徑", "autoDownload": "自動下載範例圖片", "autoDownloadHelp": "自動為沒有範例圖片的模型下載範例圖片(需設定下載位置)", + "openMode": "開啟範例圖片動作", + "openModeHelp": "選擇是在伺服器上開啟、複製對應的本機路徑,或啟動自訂 URI。", + "openModeOptions": { + "system": "在伺服器上開啟", + "clipboard": "複製本機路徑", + "uriTemplate": "開啟自訂 URI" + }, + "localRoot": "本機範例圖片根目錄", + "localRootHelp": "可選的本機或掛載根目錄,用於對應伺服器上的範例圖片目錄。若留白,則會重用伺服器路徑。", + "localRootPlaceholder": "例如:/Volumes/ComfyUI/example_images", + "uriTemplate": "開啟 URI 範本", + "uriTemplateHelp": "使用自訂深層連結,例如檔案 URI 或 Shortcuts 連結。", + "uriTemplatePlaceholder": "例如:shortcuts://run-shortcut?name=Open%20Finder&input=text&text={{encoded_local_path}}", + "uriTemplatePlaceholders": "可用佔位符:{{local_path}}、{{encoded_local_path}}、{{relative_path}}、{{encoded_relative_path}}、{{file_uri}}、{{encoded_file_uri}}", "optimizeImages": "最佳化下載圖片", "optimizeImagesHelp": "最佳化範例圖片以減少檔案大小並提升載入速度(會保留原有的 metadata)", "download": "下載", @@ -1442,6 +1456,10 @@ "opened": "範例圖片資料夾已開啟", "openingFolder": "正在開啟範例圖片資料夾", "failedToOpen": "開啟範例圖片資料夾失敗", + "copiedPath": "路徑已複製到剪貼簿:{{path}}", + "clipboardFallback": "路徑:{{path}}", + "copiedUri": "連結已複製到剪貼簿:{{uri}}", + "uriClipboardFallback": "連結:{{uri}}", "setupRequired": "範例圖片儲存", "setupDescription": "要新增自訂範例圖片,您需要先設定下載位置。", "setupUsage": "此路徑用於儲存下載的範例圖片和自訂圖片。", diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index b91b303f..16cc2860 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -81,6 +81,9 @@ DEFAULT_SETTINGS: Dict[str, Any] = { "folder_paths": {}, "extra_folder_paths": {}, "example_images_path": "", + "example_images_open_mode": "system", + "example_images_local_root": "", + "example_images_open_uri_template": "", "optimize_example_images": True, "auto_download_example_images": False, "blur_mature_content": True, diff --git a/py/utils/example_images_file_manager.py b/py/utils/example_images_file_manager.py index 8e66472a..c8794522 100644 --- a/py/utils/example_images_file_manager.py +++ b/py/utils/example_images_file_manager.py @@ -1,17 +1,81 @@ import logging import os -import sys +import re import subprocess +import sys +from urllib.parse import quote + from aiohttp import web from ..services.settings_manager import get_settings_manager from ..utils.example_images_paths import ( get_model_folder, - get_model_relative_path, ) from ..utils.constants import SUPPORTED_MEDIA_EXTENSIONS logger = logging.getLogger(__name__) + +_WINDOWS_DRIVE_PATTERN = re.compile(r"^[A-Za-z]:/") + + +def _is_within_root(path: str, root: str) -> bool: + try: + return os.path.commonpath([os.path.abspath(path), os.path.abspath(root)]) == os.path.abspath(root) + except ValueError: + return False + + +def _join_local_example_path(local_root: str, relative_path: str) -> str: + separator = "\\" if "\\" in local_root and "/" not in local_root else "/" + normalized_root = local_root.rstrip("\\/") + normalized_relative = relative_path.replace("/", separator) + if not normalized_root: + return normalized_relative + return f"{normalized_root}{separator}{normalized_relative}" + + +def _build_file_uri(path: str) -> str: + normalized = path.replace("\\", "/") + if _WINDOWS_DRIVE_PATTERN.match(normalized): + return f"file:///{quote(normalized, safe='/:')}" + if normalized.startswith("/"): + return f"file://{quote(normalized, safe='/:')}" + return f"file:///{quote(normalized.lstrip('/'), safe='/:')}" + + +def _render_open_uri_template(template: str, local_path: str, relative_path: str) -> str: + file_uri = _build_file_uri(local_path) + replacements = { + "{{local_path}}": local_path, + "{{encoded_local_path}}": quote(local_path, safe=""), + "{{relative_path}}": relative_path, + "{{encoded_relative_path}}": quote(relative_path, safe=""), + "{{file_uri}}": file_uri, + "{{encoded_file_uri}}": quote(file_uri, safe=""), + } + + rendered = template + for placeholder, value in replacements.items(): + rendered = rendered.replace(placeholder, value) + return rendered + + +def _open_system_folder(model_folder: str) -> dict[str, object]: + if os.name == "nt": # Windows + os.startfile(model_folder) + elif os.name == "posix": # macOS and Linux + if sys.platform == "darwin": # macOS + subprocess.Popen(["open", model_folder]) + else: # Linux + subprocess.Popen(["xdg-open", model_folder]) + + return { + "success": True, + "message": f"Opened example images folder for {model_folder}", + "path": model_folder, + } + + class ExampleImagesFileManager: """Manages access and operations for example image files""" @@ -54,7 +118,7 @@ class ExampleImagesFileManager: }, status=500) # Path validation: ensure model_folder is under example_images_path - if not model_folder.startswith(os.path.abspath(example_images_path)): + if not _is_within_root(model_folder, example_images_path): return web.json_response({ 'success': False, 'error': 'Invalid model folder path' @@ -66,20 +130,40 @@ class ExampleImagesFileManager: 'success': False, 'error': 'No example images found for this model. Download example images first.' }, status=404) - - # Open folder in file explorer - if os.name == 'nt': # Windows - os.startfile(model_folder) - elif os.name == 'posix': # macOS and Linux - if sys.platform == 'darwin': # macOS - subprocess.Popen(['open', model_folder]) - else: # Linux - subprocess.Popen(['xdg-open', model_folder]) - - return web.json_response({ - 'success': True, - 'message': f'Opened example images folder for model {model_hash}' - }) + + root_path = os.path.abspath(example_images_path) + relative_path = os.path.relpath(model_folder, root_path).replace("\\", "/") + open_mode = settings_manager.get("example_images_open_mode") or "system" + + if open_mode == "clipboard": + local_root = settings_manager.get("example_images_local_root") or root_path + local_path = _join_local_example_path(local_root, relative_path) + return web.json_response({ + 'success': True, + 'mode': 'clipboard', + 'path': local_path, + 'relative_path': relative_path, + }) + + if open_mode == "uri_template": + local_root = settings_manager.get("example_images_local_root") or root_path + uri_template = settings_manager.get("example_images_open_uri_template") or "" + if not uri_template.strip(): + return web.json_response({ + 'success': False, + 'error': 'No example image open URI template configured.' + }, status=400) + + local_path = _join_local_example_path(local_root, relative_path) + return web.json_response({ + 'success': True, + 'mode': 'uri', + 'path': local_path, + 'relative_path': relative_path, + 'uri': _render_open_uri_template(uri_template, local_path, relative_path), + }) + + return web.json_response(_open_system_folder(model_folder)) except Exception as e: logger.error(f"Failed to open example images folder: {e}", exc_info=True) @@ -143,7 +227,7 @@ class ExampleImagesFileManager: file_ext = os.path.splitext(file)[1].lower() if (file_ext in SUPPORTED_MEDIA_EXTENSIONS['images'] or file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos']): - relative_path = get_model_relative_path(model_hash) + relative_path = os.path.relpath(model_folder, os.path.abspath(example_images_path)).replace("\\", "/") files.append({ 'name': file, 'path': f'/example_images_static/{relative_path}/{file}', @@ -227,4 +311,4 @@ class ExampleImagesFileManager: return web.json_response({ 'has_images': False, 'error': str(e) - }) \ No newline at end of file + }) diff --git a/settings.json.example b/settings.json.example index 47afdde1..5a7f63fd 100644 --- a/settings.json.example +++ b/settings.json.example @@ -15,5 +15,8 @@ "C:/path/to/another/embeddings_folder" ] }, + "example_images_open_mode": "system", + "example_images_local_root": "", + "example_images_open_uri_template": "", "auto_organize_exclusions": [] } diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index ab2906b2..4cd92125 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -914,6 +914,23 @@ export class SettingsManager { autoDownloadExampleImagesCheckbox.checked = state.global.settings.auto_download_example_images || false; } + const exampleImagesOpenModeSelect = document.getElementById('exampleImagesOpenMode'); + if (exampleImagesOpenModeSelect) { + exampleImagesOpenModeSelect.value = state.global.settings.example_images_open_mode || 'system'; + } + + const exampleImagesLocalRootInput = document.getElementById('exampleImagesLocalRoot'); + if (exampleImagesLocalRootInput) { + exampleImagesLocalRootInput.value = state.global.settings.example_images_local_root || ''; + } + + const exampleImagesOpenUriTemplateInput = document.getElementById('exampleImagesOpenUriTemplate'); + if (exampleImagesOpenUriTemplateInput) { + exampleImagesOpenUriTemplateInput.value = state.global.settings.example_images_open_uri_template || ''; + } + + this.updateExampleImagesOpenSettingsVisibility(); + // Load download path templates this.loadDownloadPathTemplates(); @@ -2015,6 +2032,25 @@ export class SettingsManager { } } + updateExampleImagesOpenSettingsVisibility() { + const openMode = state.global.settings.example_images_open_mode || 'system'; + const localRootSetting = document.getElementById('exampleImagesLocalRootSetting'); + const uriTemplateSetting = document.getElementById('exampleImagesUriTemplateSetting'); + + if (localRootSetting) { + localRootSetting.style.display = openMode === 'system' ? 'none' : 'block'; + } + + if (uriTemplateSetting) { + uriTemplateSetting.style.display = openMode === 'uri_template' ? 'block' : 'none'; + } + } + + async handleExampleImagesOpenModeChange() { + await this.saveSelectSetting('exampleImagesOpenMode', 'example_images_open_mode'); + this.updateExampleImagesOpenSettingsVisibility(); + } + async loadMetadataArchiveSettings() { try { // Load current settings from state diff --git a/static/js/state/index.js b/static/js/state/index.js index 5206c3d4..0729ca20 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -25,6 +25,9 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({ base_model_path_mappings: {}, download_path_templates: {}, example_images_path: '', + example_images_open_mode: 'system', + example_images_local_root: '', + example_images_open_uri_template: '', optimize_example_images: true, auto_download_example_images: false, blur_mature_content: true, diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index bd0ca852..45548994 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -64,6 +64,33 @@ export function openCivitaiUrl(url) { return window.open(url, '_blank', 'noopener,noreferrer'); } +async function copyExampleImagesValue(value, successKey, fallbackKey, paramsKey = 'path') { + if (!value) { + return false; + } + + const params = { [paramsKey]: value }; + try { + await navigator.clipboard.writeText(value); + showToast(successKey, params, 'success'); + return true; + } catch (clipboardErr) { + console.warn('Clipboard API not available:', clipboardErr); + showToast(fallbackKey, params, 'info'); + return false; + } +} + +function tryOpenExternalUri(uri) { + try { + const openedWindow = window.open(uri, '_blank', 'noopener,noreferrer'); + return openedWindow !== null; + } catch (error) { + console.warn('Failed to open external URI:', error); + return false; + } +} + /** * Utility function to copy text to clipboard with fallback for older browsers * @param {string} text - The text to copy to clipboard @@ -1088,7 +1115,31 @@ export async function openExampleImagesFolder(modelHash) { const result = await response.json(); if (result.success) { - const message = translate('uiHelpers.exampleImages.openingFolder', {}, 'Opening example images folder'); + if (result.mode === 'clipboard' && result.path) { + await copyExampleImagesValue( + result.path, + 'uiHelpers.exampleImages.copiedPath', + 'uiHelpers.exampleImages.clipboardFallback', + 'path' + ); + return true; + } + + if (result.mode === 'uri' && result.uri) { + const opened = tryOpenExternalUri(result.uri); + if (!opened) { + await copyExampleImagesValue( + result.uri, + 'uiHelpers.exampleImages.copiedUri', + 'uiHelpers.exampleImages.uriClipboardFallback', + 'uri' + ); + } else { + showToast('uiHelpers.exampleImages.opened', {}, 'success'); + } + return true; + } + showToast('uiHelpers.exampleImages.opened', {}, 'success'); return true; } else { diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index 607241e0..3a5c0a8d 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -1081,7 +1081,7 @@ - +
@@ -1099,6 +1099,60 @@
+ +
+
+
+ +
+
+ +
+
+
+ + + + diff --git a/tests/frontend/managers/settingsManager.library.test.js b/tests/frontend/managers/settingsManager.library.test.js index 2b8eb9a0..f14c470c 100644 --- a/tests/frontend/managers/settingsManager.library.test.js +++ b/tests/frontend/managers/settingsManager.library.test.js @@ -340,4 +340,52 @@ describe('SettingsManager library controls', () => { expect(aria2PathSetting.style.display).toBe('none'); expect(saveSpy).toHaveBeenCalledWith('downloadBackend', 'download_backend'); }); + + it('loads example image remote-open settings and updates field visibility', async () => { + const manager = createManager(); + document.body.innerHTML = ` + + + + + + `; + + vi.spyOn(manager, 'loadMetadataArchiveSettings').mockResolvedValue(); + vi.spyOn(manager, 'loadBackupSettings').mockResolvedValue(); + vi.spyOn(manager, 'loadLibraries').mockResolvedValue(); + vi.spyOn(manager, 'loadLoraRoots').mockResolvedValue(); + vi.spyOn(manager, 'loadCheckpointRoots').mockResolvedValue(); + vi.spyOn(manager, 'loadUnetRoots').mockResolvedValue(); + vi.spyOn(manager, 'loadEmbeddingRoots').mockResolvedValue(); + + state.global.settings = { + example_images_open_mode: 'uri_template', + example_images_local_root: '/Volumes/ComfyUI/examples', + example_images_open_uri_template: 'shortcuts://run-shortcut?text={{encoded_local_path}}', + }; + + await manager.loadSettingsToUI(); + + expect(document.getElementById('exampleImagesOpenMode').value).toBe('uri_template'); + expect(document.getElementById('exampleImagesLocalRoot').value).toBe('/Volumes/ComfyUI/examples'); + expect(document.getElementById('exampleImagesOpenUriTemplate').value) + .toBe('shortcuts://run-shortcut?text={{encoded_local_path}}'); + expect(document.getElementById('exampleImagesLocalRootSetting').style.display).toBe('block'); + expect(document.getElementById('exampleImagesUriTemplateSetting').style.display).toBe('block'); + + state.global.settings.example_images_open_mode = 'clipboard'; + manager.updateExampleImagesOpenSettingsVisibility(); + expect(document.getElementById('exampleImagesLocalRootSetting').style.display).toBe('block'); + expect(document.getElementById('exampleImagesUriTemplateSetting').style.display).toBe('none'); + + state.global.settings.example_images_open_mode = 'system'; + manager.updateExampleImagesOpenSettingsVisibility(); + expect(document.getElementById('exampleImagesLocalRootSetting').style.display).toBe('none'); + expect(document.getElementById('exampleImagesUriTemplateSetting').style.display).toBe('none'); + }); }); diff --git a/tests/frontend/utils/uiHelpers.dom.test.js b/tests/frontend/utils/uiHelpers.dom.test.js index ff8e5080..23b40ebe 100644 --- a/tests/frontend/utils/uiHelpers.dom.test.js +++ b/tests/frontend/utils/uiHelpers.dom.test.js @@ -84,6 +84,8 @@ describe('UI helper DOM utilities', () => { afterEach(() => { vi.useRealTimers(); delete global.fetch; + delete navigator.clipboard; + delete window.open; }); it('creates toast elements and cleans them up after timeout', async () => { @@ -230,4 +232,49 @@ describe('UI helper DOM utilities', () => { 'noopener,noreferrer' ); }); + + it('copies mapped local example-image paths when the backend requests clipboard mode', async () => { + global.fetch = vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + mode: 'clipboard', + path: '/Volumes/ComfyUI/examples/demo', + }), + }); + navigator.clipboard = { + writeText: vi.fn().mockResolvedValue(), + }; + + const { openExampleImagesFolder } = await import(UI_HELPERS_MODULE); + + const result = await openExampleImagesFolder('abc123'); + + expect(result).toBe(true); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('/Volumes/ComfyUI/examples/demo'); + expect(global.fetch).toHaveBeenCalledWith('/api/lm/open-example-images-folder', expect.objectContaining({ + method: 'POST', + })); + }); + + it('opens custom URIs for example-image folders when requested by the backend', async () => { + global.fetch = vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + mode: 'uri', + uri: 'shortcuts://run-shortcut?name=OpenFinder', + }), + }); + window.open = vi.fn(() => ({})); + + const { openExampleImagesFolder } = await import(UI_HELPERS_MODULE); + + const result = await openExampleImagesFolder('abc123'); + + expect(result).toBe(true); + expect(window.open).toHaveBeenCalledWith( + 'shortcuts://run-shortcut?name=OpenFinder', + '_blank', + 'noopener,noreferrer' + ); + }); }); diff --git a/tests/utils/test_example_images_file_manager.py b/tests/utils/test_example_images_file_manager.py index 45063b81..bc4129ef 100644 --- a/tests/utils/test_example_images_file_manager.py +++ b/tests/utils/test_example_images_file_manager.py @@ -66,6 +66,97 @@ async def test_open_folder_requires_existing_model_directory(monkeypatch: pytest assert model_hash in popen_calls[0][-1] +async def test_open_folder_returns_clipboard_mode_with_mapped_local_path( + monkeypatch: pytest.MonkeyPatch, tmp_path +) -> None: + settings_manager = get_settings_manager() + settings_manager.settings["example_images_path"] = str(tmp_path) + settings_manager.settings["example_images_open_mode"] = "clipboard" + settings_manager.settings["example_images_local_root"] = "/Volumes/ComfyUI/examples" + model_hash = "d" * 64 + model_folder = tmp_path / "library-a" / model_hash + model_folder.mkdir(parents=True) + (model_folder / "image.png").write_text("data", encoding="utf-8") + + popen_calls: list[list[str]] = [] + + class DummyPopen: + def __init__(self, cmd, *_args, **_kwargs): + popen_calls.append(cmd) + + monkeypatch.setattr("subprocess.Popen", DummyPopen) + monkeypatch.setattr("py.utils.example_images_file_manager.get_model_folder", lambda _hash: str(model_folder)) + + request = JsonRequest({"model_hash": model_hash}) + response = await ExampleImagesFileManager.open_folder(request) + body = json.loads(response.text) + + assert response.status == 200 + assert body == { + "success": True, + "mode": "clipboard", + "path": f"/Volumes/ComfyUI/examples/library-a/{model_hash}", + "relative_path": f"library-a/{model_hash}", + } + assert popen_calls == [] + + +async def test_open_folder_returns_uri_mode_with_rendered_template( + monkeypatch: pytest.MonkeyPatch, tmp_path +) -> None: + settings_manager = get_settings_manager() + settings_manager.settings["example_images_path"] = str(tmp_path) + settings_manager.settings["example_images_open_mode"] = "uri_template" + settings_manager.settings["example_images_local_root"] = "/Volumes/ComfyUI/examples" + settings_manager.settings["example_images_open_uri_template"] = ( + "shortcuts://run-shortcut?name=OpenFinder&input=text&text={{encoded_local_path}}" + ) + model_hash = "e" * 64 + model_folder = tmp_path / model_hash + model_folder.mkdir() + (model_folder / "image.png").write_text("data", encoding="utf-8") + + popen_calls: list[list[str]] = [] + + class DummyPopen: + def __init__(self, cmd, *_args, **_kwargs): + popen_calls.append(cmd) + + monkeypatch.setattr("subprocess.Popen", DummyPopen) + + request = JsonRequest({"model_hash": model_hash}) + response = await ExampleImagesFileManager.open_folder(request) + body = json.loads(response.text) + + assert response.status == 200 + assert body["success"] is True + assert body["mode"] == "uri" + assert body["path"] == f"/Volumes/ComfyUI/examples/{model_hash}" + assert body["relative_path"] == model_hash + assert body["uri"] == ( + "shortcuts://run-shortcut?name=OpenFinder&input=text&text=" + f"%2FVolumes%2FComfyUI%2Fexamples%2F{model_hash}" + ) + assert popen_calls == [] + + +async def test_open_folder_rejects_missing_uri_template(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + settings_manager = get_settings_manager() + settings_manager.settings["example_images_path"] = str(tmp_path) + settings_manager.settings["example_images_open_mode"] = "uri_template" + model_hash = "f" * 64 + model_folder = tmp_path / model_hash + model_folder.mkdir() + (model_folder / "image.png").write_text("data", encoding="utf-8") + + response = await ExampleImagesFileManager.open_folder(JsonRequest({"model_hash": model_hash})) + body = json.loads(response.text) + + assert response.status == 400 + assert body["success"] is False + assert body["error"] == "No example image open URI template configured." + + async def test_open_folder_rejects_invalid_paths(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: settings_manager = get_settings_manager() settings_manager.settings["example_images_path"] = str(tmp_path)