feat: add folder browser context menu and local model deduplication
- Add right-click 'Open Folder' context menu to folder browser rows and root group headers, with left-click guards to prevent context actions on non-primary clicks - Deduplicate local model entries before filtering using path-based identity and category ranking to prefer the active download category
This commit is contained in:
@@ -2083,6 +2083,15 @@ export const downloadTargetMethods = {
|
||||
if (!clean) return 'Default root';
|
||||
return clean.split(/[\\\/]+/).filter(Boolean).pop() || clean;
|
||||
};
|
||||
const getFolderBrowserContextAttrs = (category = '', subfolder = '', baseDirectory = '', tooltip = 'Right-click to open this folder') => {
|
||||
const context = this.getDownloadTargetFolderContext(category, subfolder, baseDirectory);
|
||||
if (!context) return '';
|
||||
const menuContext = {
|
||||
...context,
|
||||
open_folder_label: 'Open Folder'
|
||||
};
|
||||
return ` data-model="${this.escapeHtml(encodeURIComponent(JSON.stringify(menuContext)))}" oncontextmenu="window.MLOpenContextMenu(event, this)" data-tooltip="${this.escapeHtml(tooltip)}"`;
|
||||
};
|
||||
|
||||
const buildFolderTree = (entries = []) => {
|
||||
const root = new Map();
|
||||
@@ -2124,6 +2133,7 @@ export const downloadTargetMethods = {
|
||||
const renderFolderTreeNodes = (
|
||||
nodeMap,
|
||||
rootGroup,
|
||||
category,
|
||||
expandedSet,
|
||||
filter,
|
||||
selectedValue,
|
||||
@@ -2137,6 +2147,7 @@ export const downloadTargetMethods = {
|
||||
const childCount = countTreeNodes(node.children);
|
||||
const nodeValue = node.entry?.value || node.path;
|
||||
const nodeBaseDirectory = node.entry?.baseDirectory || rootGroup.baseDirectory || '';
|
||||
const contextAttrs = getFolderBrowserContextAttrs(category, nodeValue, nodeBaseDirectory);
|
||||
const isSelected = normalizeSubfolderPath(nodeValue).toLowerCase() === selectedValue.toLowerCase()
|
||||
&& String(nodeBaseDirectory || '') === String(selectedBaseDirectory || '');
|
||||
const shouldExpand = hasChildren && (
|
||||
@@ -2148,7 +2159,7 @@ export const downloadTargetMethods = {
|
||||
? `<button class="mr-folder-browser-toggle ${shouldExpand ? 'is-expanded' : ''}" type="button" data-browser-action="toggle" data-state-key="${encodeURIComponent(stateKey)}" aria-label="${shouldExpand ? 'Collapse folder' : 'Expand folder'}"><span class="mr-folder-browser-chevron"></span></button>`
|
||||
: `<span class="mr-folder-browser-toggle mr-folder-browser-toggle-empty"></span>`;
|
||||
const children = hasChildren
|
||||
? `<div class="mr-folder-browser-children ${shouldExpand ? 'is-expanded' : ''}">${renderFolderTreeNodes(node.children, rootGroup, expandedSet, filter, selectedValue, selectedBaseDirectory)}</div>`
|
||||
? `<div class="mr-folder-browser-children ${shouldExpand ? 'is-expanded' : ''}">${renderFolderTreeNodes(node.children, rootGroup, category, expandedSet, filter, selectedValue, selectedBaseDirectory)}</div>`
|
||||
: '';
|
||||
const rowClass = [
|
||||
'mr-folder-browser-row',
|
||||
@@ -2161,7 +2172,7 @@ export const downloadTargetMethods = {
|
||||
|
||||
return `
|
||||
<div class="mr-folder-browser-node">
|
||||
<div class="${rowClass}" data-browser-action="select" data-value="${encodeURIComponent(nodeValue)}" data-base-directory="${encodeURIComponent(nodeBaseDirectory)}"${rowStateAttribute}>
|
||||
<div class="${rowClass}" data-browser-action="select" data-value="${encodeURIComponent(nodeValue)}" data-base-directory="${encodeURIComponent(nodeBaseDirectory)}"${rowStateAttribute}${contextAttrs}>
|
||||
${toggle}
|
||||
<span class="mr-folder-browser-folder-icon">${getSvgIcon('folderOpen', 'currentColor', 'mr-folder-browser-svg')}</span>
|
||||
<span class="mr-folder-browser-name">${this.escapeHtml(node.name)}</span>
|
||||
@@ -2235,15 +2246,17 @@ export const downloadTargetMethods = {
|
||||
const treeHtml = renderFolderTreeNodes(
|
||||
tree,
|
||||
group,
|
||||
category,
|
||||
expandedSet,
|
||||
normalizedFilter,
|
||||
selectedValue,
|
||||
selectedBaseDirectory
|
||||
);
|
||||
const rootContextAttrs = getFolderBrowserContextAttrs(category, '', group.baseDirectory);
|
||||
|
||||
return `
|
||||
<div class="mr-folder-browser-root">
|
||||
<button class="mr-folder-browser-root-head ${isExpanded ? 'is-expanded' : ''}" type="button" data-browser-action="toggle" data-state-key="${encodeURIComponent(stateKey)}">
|
||||
<button class="mr-folder-browser-root-head ${isExpanded ? 'is-expanded' : ''}" type="button" data-browser-action="toggle" data-state-key="${encodeURIComponent(stateKey)}"${rootContextAttrs}>
|
||||
<span class="mr-folder-browser-chevron"></span>
|
||||
<span class="mr-folder-browser-root-title">${this.escapeHtml(group.label)}</span>
|
||||
<span class="mr-folder-browser-root-count">${group.entries.length}</span>
|
||||
@@ -2315,6 +2328,7 @@ export const downloadTargetMethods = {
|
||||
|
||||
targetEl.querySelectorAll('[data-browser-action="toggle"]').forEach(button => {
|
||||
button.addEventListener('mousedown', (event) => {
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggleFolderBrowserState(button.dataset.stateKey || '');
|
||||
@@ -2323,6 +2337,7 @@ export const downloadTargetMethods = {
|
||||
|
||||
targetEl.querySelectorAll('[data-browser-action="select"]').forEach(row => {
|
||||
row.addEventListener('mousedown', (event) => {
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (isFolderBrowserToggleZoneClick(event, row)) {
|
||||
|
||||
@@ -783,6 +783,52 @@ export const missingBrowserMethods = {
|
||||
model.filename || '',
|
||||
model.category || ''
|
||||
].map(value => String(value || '').trim().toLowerCase()).join('::');
|
||||
const getLocalModelDedupeIdentity = (entry = {}) => {
|
||||
const path = String(entry.fullPath || entry.model?.path || '').trim();
|
||||
if (path) {
|
||||
return path.replace(/[\\/]+/g, '/').toLowerCase();
|
||||
}
|
||||
return [
|
||||
entry.baseDirectory || '',
|
||||
entry.relativePath || entry.model?.relative_path || entry.model?.filename || ''
|
||||
].map(value => String(value || '').trim().replace(/[\\/]+/g, '/').toLowerCase()).join('::');
|
||||
};
|
||||
const getLocalModelCategoryRank = (category = '', preferredCategory = '') => {
|
||||
const normalized = this.normalizeDownloadCategory(category || '');
|
||||
if (normalized && normalized === preferredCategory) return 0;
|
||||
const ranks = {
|
||||
checkpoints: 10,
|
||||
loras: 10,
|
||||
vae: 10,
|
||||
diffusion_models: 10,
|
||||
text_encoders: 10,
|
||||
controlnet: 10,
|
||||
clip: 20,
|
||||
clip_gguf: 30
|
||||
};
|
||||
return ranks[normalized] ?? 40;
|
||||
};
|
||||
const dedupeLocalModelEntries = (entries = [], preferredCategory = '') => {
|
||||
const byIdentity = new Map();
|
||||
entries.forEach(entry => {
|
||||
const identity = getLocalModelDedupeIdentity(entry);
|
||||
if (!identity) return;
|
||||
const previous = byIdentity.get(identity);
|
||||
if (!previous) {
|
||||
byIdentity.set(identity, entry);
|
||||
return;
|
||||
}
|
||||
const currentRank = getLocalModelCategoryRank(entry.category, preferredCategory);
|
||||
const previousRank = getLocalModelCategoryRank(previous.category, preferredCategory);
|
||||
if (
|
||||
currentRank < previousRank
|
||||
|| (currentRank === previousRank && String(entry.category || '').localeCompare(String(previous.category || '')) < 0)
|
||||
) {
|
||||
byIdentity.set(identity, entry);
|
||||
}
|
||||
});
|
||||
return Array.from(byIdentity.values());
|
||||
};
|
||||
const getBaseDirectoryLabel = (baseDirectory = '') => {
|
||||
const clean = String(baseDirectory || '').replace(/[\\\/]+$/, '');
|
||||
if (!clean) return 'Default root';
|
||||
@@ -1043,7 +1089,10 @@ export const missingBrowserMethods = {
|
||||
const previousScrollTop = options.preserveScroll && previousScrollEl ? previousScrollEl.scrollTop : 0;
|
||||
const allModels = getAllModels();
|
||||
const activePreferredCategory = getPreferredModelCategory();
|
||||
const allEntries = allModels.map((model, index) => makeModelEntry(model, index, activePreferredCategory));
|
||||
const allEntries = dedupeLocalModelEntries(
|
||||
allModels.map((model, index) => makeModelEntry(model, index, activePreferredCategory)),
|
||||
activePreferredCategory
|
||||
);
|
||||
const rawFilter = String(filterText || '').trim();
|
||||
const filter = rawFilter.toLowerCase();
|
||||
const tokens = filter.split(/\s+/).filter(Boolean);
|
||||
|
||||
Reference in New Issue
Block a user