mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(selection): implement marquee selection for bulk operations
This commit is contained in:
@@ -318,14 +318,13 @@
|
||||
"bulkOperations": {
|
||||
"selected": "{count} ausgewählt",
|
||||
"selectedSuffix": "ausgewählt",
|
||||
"viewSelected": "Klicken Sie, um ausgewählte Elemente anzuzeigen",
|
||||
"addTags": "Tags hinzufügen",
|
||||
"sendToWorkflow": "An Workflow senden",
|
||||
"copyAll": "Alle kopieren",
|
||||
"refreshAll": "Alle aktualisieren",
|
||||
"moveAll": "Alle verschieben",
|
||||
"deleteAll": "Alle löschen",
|
||||
"clear": "Leeren"
|
||||
"viewSelected": "Auswahl anzeigen",
|
||||
"addTags": "Allen Tags hinzufügen",
|
||||
"copyAll": "Alle Syntax kopieren",
|
||||
"refreshAll": "Alle Metadaten aktualisieren",
|
||||
"moveAll": "Alle in Ordner verschieben",
|
||||
"deleteAll": "Alle Modelle löschen",
|
||||
"clear": "Auswahl löschen"
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Civitai-Daten aktualisieren",
|
||||
@@ -983,6 +982,7 @@
|
||||
"deleteFailed": "Fehler: {error}",
|
||||
"deleteFailedGeneral": "Fehler beim Löschen der Modelle",
|
||||
"selectedAdditional": "{count} zusätzliche {type}(s) ausgewählt",
|
||||
"marqueeSelectionComplete": "{count} {type}(s) mit Rahmenauswahl ausgewählt",
|
||||
"refreshMetadataFailed": "Fehler beim Aktualisieren der Metadaten",
|
||||
"nameCannotBeEmpty": "Modellname darf nicht leer sein",
|
||||
"nameUpdatedSuccessfully": "Modellname erfolgreich aktualisiert",
|
||||
|
||||
@@ -318,14 +318,13 @@
|
||||
"bulkOperations": {
|
||||
"selected": "{count} selected",
|
||||
"selectedSuffix": "selected",
|
||||
"viewSelected": "Click to view selected items",
|
||||
"addTags": "Add Tags",
|
||||
"sendToWorkflow": "Send to Workflow",
|
||||
"copyAll": "Copy All",
|
||||
"refreshAll": "Refresh All",
|
||||
"moveAll": "Move All",
|
||||
"deleteAll": "Delete All",
|
||||
"clear": "Clear"
|
||||
"viewSelected": "View Selected",
|
||||
"addTags": "Add Tags to All",
|
||||
"copyAll": "Copy All Syntax",
|
||||
"refreshAll": "Refresh All Metadata",
|
||||
"moveAll": "Move All to Folder",
|
||||
"deleteAll": "Delete All Models",
|
||||
"clear": "Clear Selection"
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Refresh Civitai Data",
|
||||
@@ -983,6 +982,7 @@
|
||||
"deleteFailed": "Error: {error}",
|
||||
"deleteFailedGeneral": "Failed to delete models",
|
||||
"selectedAdditional": "Selected {count} additional {type}(s)",
|
||||
"marqueeSelectionComplete": "Selected {count} {type}(s) with marquee selection",
|
||||
"refreshMetadataFailed": "Failed to refresh metadata",
|
||||
"nameCannotBeEmpty": "Model name cannot be empty",
|
||||
"nameUpdatedSuccessfully": "Model name updated successfully",
|
||||
|
||||
@@ -318,14 +318,13 @@
|
||||
"bulkOperations": {
|
||||
"selected": "{count} seleccionados",
|
||||
"selectedSuffix": "seleccionados",
|
||||
"viewSelected": "Clic para ver elementos seleccionados",
|
||||
"addTags": "Añadir etiquetas",
|
||||
"sendToWorkflow": "Enviar al flujo de trabajo",
|
||||
"copyAll": "Copiar todo",
|
||||
"refreshAll": "Actualizar todo",
|
||||
"moveAll": "Mover todo",
|
||||
"deleteAll": "Eliminar todo",
|
||||
"clear": "Limpiar"
|
||||
"viewSelected": "Ver seleccionados",
|
||||
"addTags": "Añadir etiquetas a todos",
|
||||
"copyAll": "Copiar toda la sintaxis",
|
||||
"refreshAll": "Actualizar todos los metadatos",
|
||||
"moveAll": "Mover todos a carpeta",
|
||||
"deleteAll": "Eliminar todos los modelos",
|
||||
"clear": "Limpiar selección"
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Actualizar datos de Civitai",
|
||||
@@ -983,6 +982,7 @@
|
||||
"deleteFailed": "Error: {error}",
|
||||
"deleteFailedGeneral": "Error al eliminar modelos",
|
||||
"selectedAdditional": "Seleccionados {count} {type}(s) adicionales",
|
||||
"marqueeSelectionComplete": "Seleccionados {count} {type}(s) con selección de marco",
|
||||
"refreshMetadataFailed": "Error al actualizar metadatos",
|
||||
"nameCannotBeEmpty": "El nombre del modelo no puede estar vacío",
|
||||
"nameUpdatedSuccessfully": "Nombre del modelo actualizado exitosamente",
|
||||
|
||||
@@ -318,14 +318,13 @@
|
||||
"bulkOperations": {
|
||||
"selected": "{count} sélectionné(s)",
|
||||
"selectedSuffix": "sélectionné(s)",
|
||||
"viewSelected": "Cliquez pour voir les éléments sélectionnés",
|
||||
"addTags": "Ajouter des tags",
|
||||
"sendToWorkflow": "Envoyer vers le workflow",
|
||||
"copyAll": "Tout copier",
|
||||
"refreshAll": "Tout actualiser",
|
||||
"moveAll": "Tout déplacer",
|
||||
"deleteAll": "Tout supprimer",
|
||||
"clear": "Effacer"
|
||||
"viewSelected": "Voir la sélection",
|
||||
"addTags": "Ajouter des tags à tous",
|
||||
"copyAll": "Copier toute la syntaxe",
|
||||
"refreshAll": "Actualiser toutes les métadonnées",
|
||||
"moveAll": "Déplacer tout vers un dossier",
|
||||
"deleteAll": "Supprimer tous les modèles",
|
||||
"clear": "Effacer la sélection"
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Actualiser les données Civitai",
|
||||
@@ -983,6 +982,7 @@
|
||||
"deleteFailed": "Erreur : {error}",
|
||||
"deleteFailedGeneral": "Échec de la suppression des modèles",
|
||||
"selectedAdditional": "{count} {type}(s) supplémentaire(s) sélectionné(s)",
|
||||
"marqueeSelectionComplete": "{count} {type}(s) sélectionné(s) avec la sélection par glisser-déposer",
|
||||
"refreshMetadataFailed": "Échec de l'actualisation des métadonnées",
|
||||
"nameCannotBeEmpty": "Le nom du modèle ne peut pas être vide",
|
||||
"nameUpdatedSuccessfully": "Nom du modèle mis à jour avec succès",
|
||||
|
||||
@@ -318,14 +318,13 @@
|
||||
"bulkOperations": {
|
||||
"selected": "{count} 選択中",
|
||||
"selectedSuffix": "選択中",
|
||||
"viewSelected": "選択したアイテムを表示するにはクリック",
|
||||
"addTags": "タグを追加",
|
||||
"sendToWorkflow": "ワークフローに送信",
|
||||
"copyAll": "すべてコピー",
|
||||
"refreshAll": "すべて更新",
|
||||
"moveAll": "すべて移動",
|
||||
"deleteAll": "すべて削除",
|
||||
"clear": "クリア"
|
||||
"viewSelected": "選択中を表示",
|
||||
"addTags": "すべてにタグを追加",
|
||||
"copyAll": "すべての構文をコピー",
|
||||
"refreshAll": "すべてのメタデータを更新",
|
||||
"moveAll": "すべてをフォルダに移動",
|
||||
"deleteAll": "すべてのモデルを削除",
|
||||
"clear": "選択をクリア"
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Civitaiデータを更新",
|
||||
@@ -983,6 +982,7 @@
|
||||
"deleteFailed": "エラー:{error}",
|
||||
"deleteFailedGeneral": "モデルの削除に失敗しました",
|
||||
"selectedAdditional": "{count} 追加{type}が選択されました",
|
||||
"marqueeSelectionComplete": "マーキー選択で {count} の{type}が選択されました",
|
||||
"refreshMetadataFailed": "メタデータの更新に失敗しました",
|
||||
"nameCannotBeEmpty": "モデル名を空にすることはできません",
|
||||
"nameUpdatedSuccessfully": "モデル名が正常に更新されました",
|
||||
|
||||
@@ -318,14 +318,13 @@
|
||||
"bulkOperations": {
|
||||
"selected": "{count}개 선택됨",
|
||||
"selectedSuffix": "개 선택됨",
|
||||
"viewSelected": "선택된 항목 보기",
|
||||
"addTags": "태그 추가",
|
||||
"sendToWorkflow": "워크플로로 전송",
|
||||
"copyAll": "모두 복사",
|
||||
"refreshAll": "모두 새로고침",
|
||||
"moveAll": "모두 이동",
|
||||
"deleteAll": "모두 삭제",
|
||||
"clear": "지우기"
|
||||
"viewSelected": "선택 항목 보기",
|
||||
"addTags": "모두에 태그 추가",
|
||||
"copyAll": "모든 문법 복사",
|
||||
"refreshAll": "모든 메타데이터 새로고침",
|
||||
"moveAll": "모두 폴더로 이동",
|
||||
"deleteAll": "모든 모델 삭제",
|
||||
"clear": "선택 지우기"
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Civitai 데이터 새로고침",
|
||||
@@ -983,6 +982,7 @@
|
||||
"deleteFailed": "오류: {error}",
|
||||
"deleteFailedGeneral": "모델 삭제에 실패했습니다",
|
||||
"selectedAdditional": "추가로 {count}개의 {type}이(가) 선택되었습니다",
|
||||
"marqueeSelectionComplete": "마키 선택으로 {count}개의 {type}이(가) 선택되었습니다",
|
||||
"refreshMetadataFailed": "메타데이터 새로고침에 실패했습니다",
|
||||
"nameCannotBeEmpty": "모델 이름은 비어있을 수 없습니다",
|
||||
"nameUpdatedSuccessfully": "모델 이름이 성공적으로 업데이트되었습니다",
|
||||
|
||||
@@ -318,14 +318,13 @@
|
||||
"bulkOperations": {
|
||||
"selected": "Выбрано {count}",
|
||||
"selectedSuffix": "выбрано",
|
||||
"viewSelected": "Нажмите для просмотра выбранных элементов",
|
||||
"addTags": "Добавить теги",
|
||||
"sendToWorkflow": "Отправить в Workflow",
|
||||
"copyAll": "Копировать все",
|
||||
"refreshAll": "Обновить все",
|
||||
"moveAll": "Переместить все",
|
||||
"deleteAll": "Удалить все",
|
||||
"clear": "Очистить"
|
||||
"viewSelected": "Просмотреть выбранные",
|
||||
"addTags": "Добавить теги ко всем",
|
||||
"copyAll": "Копировать весь синтаксис",
|
||||
"refreshAll": "Обновить все метаданные",
|
||||
"moveAll": "Переместить все в папку",
|
||||
"deleteAll": "Удалить все модели",
|
||||
"clear": "Очистить выбор"
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Обновить данные Civitai",
|
||||
@@ -983,6 +982,7 @@
|
||||
"deleteFailed": "Ошибка: {error}",
|
||||
"deleteFailedGeneral": "Не удалось удалить модели",
|
||||
"selectedAdditional": "Выбрано дополнительно {count} {type}(ей)",
|
||||
"marqueeSelectionComplete": "Выбрано {count} {type} с помощью выделения рамкой",
|
||||
"refreshMetadataFailed": "Не удалось обновить метаданные",
|
||||
"nameCannotBeEmpty": "Название модели не может быть пустым",
|
||||
"nameUpdatedSuccessfully": "Название модели успешно обновлено",
|
||||
|
||||
@@ -318,14 +318,13 @@
|
||||
"bulkOperations": {
|
||||
"selected": "已选中 {count} 项",
|
||||
"selectedSuffix": "已选中",
|
||||
"viewSelected": "点击查看已选项目",
|
||||
"addTags": "批量添加标签",
|
||||
"sendToWorkflow": "发送到工作流",
|
||||
"copyAll": "全部复制",
|
||||
"refreshAll": "全部刷新",
|
||||
"moveAll": "全部移动",
|
||||
"deleteAll": "全部删除",
|
||||
"clear": "清除"
|
||||
"viewSelected": "查看已选中",
|
||||
"addTags": "为所有添加标签",
|
||||
"copyAll": "复制全部语法",
|
||||
"refreshAll": "刷新全部元数据",
|
||||
"moveAll": "全部移动到文件夹",
|
||||
"deleteAll": "删除所有模型",
|
||||
"clear": "清除选择"
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "刷新 Civitai 数据",
|
||||
@@ -983,6 +982,7 @@
|
||||
"deleteFailed": "错误:{error}",
|
||||
"deleteFailedGeneral": "删除模型失败",
|
||||
"selectedAdditional": "已选中 {count} 个额外 {type}",
|
||||
"marqueeSelectionComplete": "框选已选中 {count} 个 {type}",
|
||||
"refreshMetadataFailed": "刷新元数据失败",
|
||||
"nameCannotBeEmpty": "模型名称不能为空",
|
||||
"nameUpdatedSuccessfully": "模型名称更新成功",
|
||||
|
||||
@@ -318,14 +318,13 @@
|
||||
"bulkOperations": {
|
||||
"selected": "已選擇 {count} 項",
|
||||
"selectedSuffix": "已選擇",
|
||||
"viewSelected": "點擊檢視已選項目",
|
||||
"addTags": "新增標籤",
|
||||
"sendToWorkflow": "傳送到工作流",
|
||||
"copyAll": "全部複製",
|
||||
"refreshAll": "全部刷新",
|
||||
"moveAll": "全部移動",
|
||||
"deleteAll": "全部刪除",
|
||||
"clear": "清除"
|
||||
"viewSelected": "檢視已選取",
|
||||
"addTags": "新增標籤到全部",
|
||||
"copyAll": "複製全部語法",
|
||||
"refreshAll": "刷新全部 metadata",
|
||||
"moveAll": "全部移動到資料夾",
|
||||
"deleteAll": "刪除全部模型",
|
||||
"clear": "清除選取"
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "刷新 Civitai 資料",
|
||||
@@ -983,6 +982,7 @@
|
||||
"deleteFailed": "錯誤:{error}",
|
||||
"deleteFailedGeneral": "刪除模型失敗",
|
||||
"selectedAdditional": "已選擇 {count} 個額外 {type}",
|
||||
"marqueeSelectionComplete": "框選已選擇 {count} 個 {type}",
|
||||
"refreshMetadataFailed": "刷新 metadata 失敗",
|
||||
"nameCannotBeEmpty": "模型名稱不可為空",
|
||||
"nameUpdatedSuccessfully": "模型名稱已成功更新",
|
||||
|
||||
@@ -1,81 +1,3 @@
|
||||
/* Bulk Operations Styles */
|
||||
.bulk-operations-panel {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateY(100px) translateX(-50%);
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-base);
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: var(--z-overlay);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 420px;
|
||||
max-width: 900px;
|
||||
width: auto;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bulk-operations-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
gap: 20px; /* Increase space between count and buttons */
|
||||
}
|
||||
|
||||
#selectedCount {
|
||||
font-weight: 500;
|
||||
background: var(--bg-color);
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bulk-operations-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bulk-operations-actions button {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
min-height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bulk-operations-actions button:hover {
|
||||
background: var(--lora-accent);
|
||||
color: white;
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
/* Danger button style - updated to use proper theme variables */
|
||||
.bulk-operations-actions button.danger-btn {
|
||||
background: oklch(70% 0.2 29); /* Light red background that works in both themes */
|
||||
color: oklch(98% 0.01 0); /* Almost white text for good contrast */
|
||||
border-color: var(--lora-error);
|
||||
}
|
||||
|
||||
.bulk-operations-actions button.danger-btn:hover {
|
||||
background: var(--lora-error);
|
||||
color: oklch(100% 0 0); /* Pure white text on hover for maximum contrast */
|
||||
}
|
||||
|
||||
/* Style for selected cards */
|
||||
.model-card.selected {
|
||||
box-shadow: 0 0 0 2px var(--lora-accent);
|
||||
@@ -99,203 +21,29 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Update bulk operations button to match others when active */
|
||||
#bulkOperationsBtn.active {
|
||||
background: var(--lora-accent);
|
||||
color: white;
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.bulk-operations-panel {
|
||||
width: calc(100% - 40px);
|
||||
min-width: unset;
|
||||
max-width: unset;
|
||||
left: 20px;
|
||||
transform: none;
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.bulk-operations-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.bulk-operations-panel.visible {
|
||||
transform: translateY(0) translateX(-50%);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Thumbnail Strip Styles */
|
||||
.selected-thumbnails-strip {
|
||||
/* Marquee selection styles */
|
||||
.marquee-selection {
|
||||
position: fixed;
|
||||
bottom: 80px; /* Position above the bulk operations panel */
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-base);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
z-index: calc(var(--z-overlay) - 1); /* Just below the bulk panel z-index */
|
||||
padding: 16px;
|
||||
max-width: 80%;
|
||||
width: auto;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
border: 2px dashed var(--lora-accent, #007bff);
|
||||
background: rgba(0, 123, 255, 0.1);
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.selected-thumbnails-strip.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
/* Visual feedback when marquee selecting */
|
||||
.marquee-selecting {
|
||||
cursor: crosshair;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.thumbnails-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px; /* Space for scrollbar */
|
||||
max-width: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.selected-thumbnail {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
min-width: 80px; /* Prevent shrinking */
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: var(--bg-color);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.selected-thumbnail:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.selected-thumbnail img,
|
||||
.selected-thumbnail video {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumbnail-name {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 3px 5px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thumbnail-remove {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.thumbnail-remove:hover {
|
||||
opacity: 1;
|
||||
background: var(--lora-error);
|
||||
}
|
||||
|
||||
.strip-close-btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.strip-close-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Style the selectedCount to indicate it's clickable */
|
||||
.selectable-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.selectable-count:hover {
|
||||
background: var(--lora-border);
|
||||
}
|
||||
|
||||
.dropdown-caret {
|
||||
font-size: 12px;
|
||||
visibility: hidden; /* Will be shown via JS when items are selected */
|
||||
}
|
||||
|
||||
/* Scrollbar styling for the thumbnails container */
|
||||
.thumbnails-container::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.thumbnails-container::-webkit-scrollbar-track {
|
||||
background: var(--bg-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.thumbnails-container::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.thumbnails-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--lora-accent);
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 768px) {
|
||||
.selected-thumbnails-strip {
|
||||
width: calc(100% - 40px);
|
||||
max-width: none;
|
||||
left: 20px;
|
||||
transform: translateY(20px);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.selected-thumbnails-strip.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.selected-thumbnail {
|
||||
width: 70px;
|
||||
min-width: 70px;
|
||||
}
|
||||
/* Prevent text selection during marquee */
|
||||
.marquee-selecting * {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
@@ -12,6 +12,12 @@ export class BulkManager {
|
||||
// Remove bulk panel references since we're using context menu now
|
||||
this.bulkContextMenu = null; // Will be set by core initialization
|
||||
|
||||
// Marquee selection properties
|
||||
this.isMarqueeActive = false;
|
||||
this.marqueeStart = { x: 0, y: 0 };
|
||||
this.marqueeElement = null;
|
||||
this.initialSelectedModels = new Set();
|
||||
|
||||
// Model type specific action configurations
|
||||
this.actionConfig = {
|
||||
[MODEL_TYPES.LORA]: {
|
||||
@@ -44,6 +50,7 @@ export class BulkManager {
|
||||
initialize() {
|
||||
this.setupEventListeners();
|
||||
this.setupGlobalKeyboardListeners();
|
||||
this.setupMarqueeSelection();
|
||||
}
|
||||
|
||||
setBulkContextMenu(bulkContextMenu) {
|
||||
@@ -734,6 +741,201 @@ export class BulkManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup marquee selection functionality
|
||||
*/
|
||||
setupMarqueeSelection() {
|
||||
const container = document.querySelector('.models-container') || document.body;
|
||||
|
||||
container.addEventListener('mousedown', (e) => {
|
||||
// Disable marquee if any modal is open
|
||||
if (modalManager.isAnyModalOpen()) {
|
||||
return;
|
||||
}
|
||||
// Only start marquee selection on left click in empty areas
|
||||
if (e.button !== 0 || e.target.closest('.model-card') || e.target.closest('button') || e.target.closest('input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent text selection during marquee
|
||||
e.preventDefault();
|
||||
|
||||
this.startMarqueeSelection(e);
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
// Disable marquee update if any modal is open
|
||||
if (modalManager.isAnyModalOpen()) {
|
||||
return;
|
||||
}
|
||||
if (this.isMarqueeActive) {
|
||||
this.updateMarqueeSelection(e);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', (e) => {
|
||||
if (this.isMarqueeActive) {
|
||||
this.endMarqueeSelection(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent context menu during marquee selection
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
if (this.isMarqueeActive) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start marquee selection
|
||||
*/
|
||||
startMarqueeSelection(e) {
|
||||
// Store initial mouse position
|
||||
this.marqueeStart.x = e.clientX;
|
||||
this.marqueeStart.y = e.clientY;
|
||||
|
||||
// Store initial selection state
|
||||
this.initialSelectedModels = new Set(state.selectedModels);
|
||||
|
||||
// Enter bulk mode if not already active
|
||||
if (!state.bulkMode) {
|
||||
this.toggleBulkMode();
|
||||
}
|
||||
|
||||
// Create marquee element
|
||||
this.createMarqueeElement();
|
||||
|
||||
this.isMarqueeActive = true;
|
||||
|
||||
// Add visual feedback class to body
|
||||
document.body.classList.add('marquee-selecting');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the visual marquee selection rectangle
|
||||
*/
|
||||
createMarqueeElement() {
|
||||
this.marqueeElement = document.createElement('div');
|
||||
this.marqueeElement.className = 'marquee-selection';
|
||||
this.marqueeElement.style.cssText = `
|
||||
position: fixed;
|
||||
border: 2px dashed var(--lora-accent, #007bff);
|
||||
background: rgba(0, 123, 255, 0.1);
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
left: ${this.marqueeStart.x}px;
|
||||
top: ${this.marqueeStart.y}px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
`;
|
||||
document.body.appendChild(this.marqueeElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update marquee selection rectangle and selected items
|
||||
*/
|
||||
updateMarqueeSelection(e) {
|
||||
if (!this.marqueeElement) return;
|
||||
|
||||
const currentX = e.clientX;
|
||||
const currentY = e.clientY;
|
||||
|
||||
// Calculate rectangle bounds
|
||||
const left = Math.min(this.marqueeStart.x, currentX);
|
||||
const top = Math.min(this.marqueeStart.y, currentY);
|
||||
const width = Math.abs(currentX - this.marqueeStart.x);
|
||||
const height = Math.abs(currentY - this.marqueeStart.y);
|
||||
|
||||
// Update marquee element position and size
|
||||
this.marqueeElement.style.left = left + 'px';
|
||||
this.marqueeElement.style.top = top + 'px';
|
||||
this.marqueeElement.style.width = width + 'px';
|
||||
this.marqueeElement.style.height = height + 'px';
|
||||
|
||||
// Check which cards intersect with marquee
|
||||
this.updateCardSelection(left, top, left + width, top + height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update card selection based on marquee bounds
|
||||
*/
|
||||
updateCardSelection(left, top, right, bottom) {
|
||||
const cards = document.querySelectorAll('.model-card');
|
||||
const newSelection = new Set(this.initialSelectedModels);
|
||||
|
||||
cards.forEach(card => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
|
||||
// Check if card intersects with marquee rectangle
|
||||
const intersects = !(rect.right < left ||
|
||||
rect.left > right ||
|
||||
rect.bottom < top ||
|
||||
rect.top > bottom);
|
||||
|
||||
const filepath = card.dataset.filepath;
|
||||
|
||||
if (intersects) {
|
||||
// Add to selection if intersecting
|
||||
newSelection.add(filepath);
|
||||
card.classList.add('selected');
|
||||
|
||||
// Cache metadata if not already cached
|
||||
const metadataCache = this.getMetadataCache();
|
||||
if (!metadataCache.has(filepath)) {
|
||||
metadataCache.set(filepath, {
|
||||
fileName: card.dataset.file_name,
|
||||
usageTips: card.dataset.usage_tips,
|
||||
previewUrl: this.getCardPreviewUrl(card),
|
||||
isVideo: this.isCardPreviewVideo(card),
|
||||
modelName: card.dataset.name
|
||||
});
|
||||
}
|
||||
} else if (!this.initialSelectedModels.has(filepath)) {
|
||||
// Remove from selection if not intersecting and wasn't initially selected
|
||||
newSelection.delete(filepath);
|
||||
card.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
|
||||
// Update global selection state
|
||||
state.selectedModels = newSelection;
|
||||
|
||||
// Update context menu header if visible
|
||||
if (this.bulkContextMenu) {
|
||||
this.bulkContextMenu.updateSelectedCountHeader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End marquee selection
|
||||
*/
|
||||
endMarqueeSelection(e) {
|
||||
this.isMarqueeActive = false;
|
||||
|
||||
// Remove marquee element
|
||||
if (this.marqueeElement) {
|
||||
this.marqueeElement.remove();
|
||||
this.marqueeElement = null;
|
||||
}
|
||||
|
||||
// Remove visual feedback class
|
||||
document.body.classList.remove('marquee-selecting');
|
||||
|
||||
// Show toast with selection count if any items were selected
|
||||
const selectionCount = state.selectedModels.size;
|
||||
if (selectionCount > 0) {
|
||||
const currentConfig = MODEL_CONFIG[state.currentPageType];
|
||||
showToast('toast.models.marqueeSelectionComplete', {
|
||||
count: selectionCount,
|
||||
type: currentConfig.displayName.toLowerCase()
|
||||
}, 'success');
|
||||
}
|
||||
|
||||
// Clear initial selection state
|
||||
this.initialSelectedModels.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const bulkManager = new BulkManager();
|
||||
|
||||
Reference in New Issue
Block a user