normalize subfolder paths to forward slashes across backend and frontend
This commit is contained in:
+5
-6
@@ -363,6 +363,7 @@ class ModelResolverExtension:
|
||||
is_path_within,
|
||||
prefer_local_base_directory,
|
||||
read_json_safe,
|
||||
split_path_segments,
|
||||
write_json_atomic,
|
||||
)
|
||||
from .core.resolver import (
|
||||
@@ -4596,7 +4597,7 @@ class ModelResolverExtension:
|
||||
)
|
||||
if target_directory and subfolder:
|
||||
target_directory = _download_os.path.join(
|
||||
target_directory, subfolder
|
||||
target_directory, *split_path_segments(subfolder)
|
||||
)
|
||||
if target_directory:
|
||||
target_path = _download_os.path.join(
|
||||
@@ -4906,9 +4907,7 @@ class ModelResolverExtension:
|
||||
)
|
||||
|
||||
def add_subfolder(rel_path, base_dir=""):
|
||||
rel_path = os.path.normpath(str(rel_path or "")).replace(
|
||||
os.sep, "\\"
|
||||
)
|
||||
rel_path = "/".join(split_path_segments(rel_path))
|
||||
if not rel_path or rel_path == ".":
|
||||
return
|
||||
base_dir = os.path.abspath(base_dir) if base_dir else ""
|
||||
@@ -4978,12 +4977,12 @@ class ModelResolverExtension:
|
||||
)
|
||||
except Exception:
|
||||
base_dir = ""
|
||||
parts = [p for p in rel_path.replace("/", "\\").split("\\") if p]
|
||||
parts = split_path_segments(rel_path)
|
||||
if len(parts) <= 1:
|
||||
continue
|
||||
current = ""
|
||||
for part in parts[:-1]:
|
||||
current = f"{current}\\{part}" if current else part
|
||||
current = f"{current}/{part}" if current else part
|
||||
add_subfolder(current, base_dir)
|
||||
|
||||
for base_dir in base_dirs:
|
||||
|
||||
+1
-1
@@ -380,7 +380,7 @@ def resolve_download_subfolder(
|
||||
) -> str:
|
||||
requested = str(requested_subfolder or "").strip()
|
||||
if requested:
|
||||
return requested
|
||||
return normalize_relative_subfolder(requested)
|
||||
|
||||
active_settings = normalize_settings(settings if settings is not None else load_settings())
|
||||
if active_settings.get("download_path_mode") != "template":
|
||||
|
||||
@@ -868,8 +868,8 @@ test('download subfolder tooltip explains automatic suggestion source', () => {
|
||||
getSavedDownloadSubfolderSuggestion,
|
||||
getCurrentDownloadSubfolderSuggestion,
|
||||
normalizeDownloadSubfolderPath,
|
||||
normalizePathToBackward(value) {
|
||||
return String(value || '').replace(/[\/]+/g, '\\');
|
||||
normalizePathToForward(value) {
|
||||
return String(value || '').replace(/\\/g, '/');
|
||||
},
|
||||
normalizeDownloadCategory(value) {
|
||||
return String(value || 'checkpoints');
|
||||
@@ -902,10 +902,33 @@ test('download subfolder tooltip explains automatic suggestion source', () => {
|
||||
|
||||
const tooltip = getDownloadSubfolderTooltip.call(dialog, { node_id: 1 }, 'loras', 'SDXL/Style');
|
||||
|
||||
assert.match(tooltip, /Suggested subfolder: SDXL\\Style/);
|
||||
assert.match(tooltip, /Suggested subfolder: SDXL\/Style/);
|
||||
assert.match(tooltip, /identified as SDXL and tagged/);
|
||||
});
|
||||
|
||||
test('portable subfolder paths use forward slashes and join to the host path style', () => {
|
||||
const normalizeDownloadSubfolderPath = eval(`(${extractMethod(downloadTargetMethodsSource, 'normalizeDownloadSubfolderPath')})`);
|
||||
const joinLocalPath = eval(`(${extractMethod(downloadTargetMethodsSource, 'joinLocalPath')})`);
|
||||
const dialog = {
|
||||
normalizeDownloadSubfolderPath,
|
||||
joinLocalPath,
|
||||
normalizePathToForward(value) {
|
||||
return String(value || '').trim().replace(/\\/g, '/');
|
||||
}
|
||||
};
|
||||
|
||||
assert.equal(normalizeDownloadSubfolderPath.call(dialog, 'Pony\\Styles'), 'Pony/Styles');
|
||||
assert.equal(normalizeDownloadSubfolderPath.call(dialog, 'Pony/Styles'), 'Pony/Styles');
|
||||
assert.equal(
|
||||
joinLocalPath.call(dialog, '/models/loras', 'Pony\\Styles'),
|
||||
'/models/loras/Pony/Styles'
|
||||
);
|
||||
assert.equal(
|
||||
joinLocalPath.call(dialog, 'C:\\models\\loras', 'Pony/Styles'),
|
||||
'C:\\models\\loras\\Pony\\Styles'
|
||||
);
|
||||
});
|
||||
|
||||
test('download subfolder tooltip identifies a folder taken from the workflow model path', () => {
|
||||
const getDownloadSubfolderSuggestionReason = eval(`(${extractMethod(downloadTargetMethodsSource, 'getDownloadSubfolderSuggestionReason')})`);
|
||||
const dialog = {
|
||||
@@ -945,8 +968,8 @@ test('download subfolder tooltip explains Suggest button choice', () => {
|
||||
getSavedDownloadSubfolderSuggestion,
|
||||
getCurrentDownloadSubfolderSuggestion,
|
||||
normalizeDownloadSubfolderPath,
|
||||
normalizePathToBackward(value) {
|
||||
return String(value || '').replace(/[\/]+/g, '\\');
|
||||
normalizePathToForward(value) {
|
||||
return String(value || '').replace(/\\/g, '/');
|
||||
},
|
||||
normalizeDownloadCategory(value) {
|
||||
return String(value || 'checkpoints');
|
||||
@@ -967,7 +990,7 @@ test('download subfolder tooltip explains Suggest button choice', () => {
|
||||
|
||||
const tooltip = getDownloadSubfolderTooltip.call(dialog, { node_id: 1 }, 'loras', 'Pony\\Styles', { saved });
|
||||
|
||||
assert.match(tooltip, /Selected with Suggest: Pony\\Styles/);
|
||||
assert.match(tooltip, /Selected with Suggest: Pony\/Styles/);
|
||||
assert.match(tooltip, /saved folder rule.*\{base_model\}\/\{first_tag\}/);
|
||||
});
|
||||
|
||||
@@ -1025,6 +1048,9 @@ test('post-search auto-fill can refresh earlier suggested subfolder', async () =
|
||||
normalizeDownloadCategory(value) {
|
||||
return String(value || '');
|
||||
},
|
||||
normalizeDownloadSubfolderPath(value) {
|
||||
return String(value || '').replace(/\\/g, '/');
|
||||
},
|
||||
getDropdownValue(element) {
|
||||
return element.dataset.value;
|
||||
},
|
||||
@@ -1062,7 +1088,7 @@ test('post-search auto-fill can refresh earlier suggested subfolder', async () =
|
||||
preferTemplate: true
|
||||
});
|
||||
|
||||
assert.equal(subfolderEl.value, 'SDXL\\style');
|
||||
assert.equal(subfolderEl.value, 'SDXL/style');
|
||||
assert.equal(saves.at(-1).subfolderSuggestionSource, 'template');
|
||||
});
|
||||
|
||||
|
||||
@@ -102,6 +102,33 @@ class PathSecurityTests(unittest.TestCase):
|
||||
mock_download_file.call_args.args[1],
|
||||
)
|
||||
|
||||
def test_download_subfolder_accepts_both_separator_styles(self):
|
||||
with tempfile.TemporaryDirectory() as model_root, patch(
|
||||
"core.downloader.get_download_directory", return_value=model_root
|
||||
), patch("core.downloader.download_file") as mock_download_file:
|
||||
expected_path = os.path.join(
|
||||
model_root,
|
||||
"Pony",
|
||||
"Styles",
|
||||
"model.safetensors",
|
||||
)
|
||||
mock_download_file.return_value = {
|
||||
"success": True,
|
||||
"download_id": "portable-subfolder",
|
||||
"path": expected_path,
|
||||
}
|
||||
|
||||
result = download_model(
|
||||
"https://example.com/model.safetensors",
|
||||
"model.safetensors",
|
||||
"loras",
|
||||
download_id="portable-subfolder",
|
||||
subfolder=r"Pony\Styles",
|
||||
)
|
||||
|
||||
self.assertTrue(result["success"])
|
||||
self.assertEqual(expected_path, mock_download_file.call_args.args[1])
|
||||
|
||||
def test_non_model_executable_download_is_rejected(self):
|
||||
self.assertFalse(is_allowed_model_download_filename("payload.exe"))
|
||||
self.assertTrue(is_allowed_model_download_filename("model.safetensors"))
|
||||
|
||||
+14
-5
@@ -1,13 +1,16 @@
|
||||
import unittest
|
||||
|
||||
from core.settings import (
|
||||
bool_setting,
|
||||
normalize_settings,
|
||||
sanitize_folder_name,
|
||||
normalize_relative_subfolder,
|
||||
normalize_download_path_template,
|
||||
_resolve_base_model_mapping,
|
||||
bool_setting,
|
||||
normalize_download_path_template,
|
||||
normalize_relative_subfolder,
|
||||
normalize_settings,
|
||||
resolve_download_subfolder,
|
||||
sanitize_folder_name,
|
||||
)
|
||||
|
||||
|
||||
class SettingsTests(unittest.TestCase):
|
||||
|
||||
def test_bool_setting(self):
|
||||
@@ -42,6 +45,12 @@ class SettingsTests(unittest.TestCase):
|
||||
self.assertEqual(normalize_relative_subfolder("sub\\dir/name"), "sub/dir/name")
|
||||
self.assertEqual(normalize_relative_subfolder(""), "")
|
||||
|
||||
def test_requested_download_subfolder_uses_forward_slashes(self):
|
||||
self.assertEqual(
|
||||
resolve_download_subfolder("loras", r"Pony\Styles"),
|
||||
"Pony/Styles",
|
||||
)
|
||||
|
||||
def test_normalize_download_path_template(self):
|
||||
self.assertEqual(normalize_download_path_template("some/{category}/template/"), "some/{category}/template")
|
||||
self.assertEqual(normalize_download_path_template(""), "")
|
||||
|
||||
@@ -571,7 +571,7 @@ export const downloadTargetMethods = {
|
||||
if (String(baseDirectory || '') !== String(saved.subfolderBaseDirectory || '')) return null;
|
||||
|
||||
return {
|
||||
value: saved.subfolder || selectedValue,
|
||||
value: selectedValue,
|
||||
baseDirectory: saved.subfolderBaseDirectory || '',
|
||||
suggestionSource: saved.subfolderSuggestionSource || '',
|
||||
template: saved.subfolderSuggestionTemplate || '',
|
||||
@@ -899,19 +899,19 @@ export const downloadTargetMethods = {
|
||||
},
|
||||
|
||||
normalizeDownloadSubfolderPath(value = '') {
|
||||
return this.normalizePathToBackward(value)
|
||||
.split('\\')
|
||||
return this.normalizePathToForward(value)
|
||||
.split('/')
|
||||
.map(part => part.trim())
|
||||
.filter(part => part && part !== '.' && part !== '..')
|
||||
.join('\\');
|
||||
.join('/');
|
||||
},
|
||||
|
||||
getDownloadSubfolderPrefixes(value = '') {
|
||||
const parts = this.normalizeDownloadSubfolderPath(value).split('\\').filter(Boolean);
|
||||
const parts = this.normalizeDownloadSubfolderPath(value).split('/').filter(Boolean);
|
||||
const prefixes = [];
|
||||
let current = '';
|
||||
parts.forEach(part => {
|
||||
current = current ? `${current}\\${part}` : part;
|
||||
current = current ? `${current}/${part}` : part;
|
||||
prefixes.push(current);
|
||||
});
|
||||
return prefixes;
|
||||
@@ -965,18 +965,19 @@ export const downloadTargetMethods = {
|
||||
},
|
||||
|
||||
getSubfolderOptionValue(option) {
|
||||
return String(
|
||||
const value = String(
|
||||
option && typeof option === 'object'
|
||||
? option.value || ''
|
||||
: option || ''
|
||||
);
|
||||
return this.normalizeDownloadSubfolderPath(value);
|
||||
},
|
||||
|
||||
getSubfolderOptionLabel(option) {
|
||||
if (option && typeof option === 'object') {
|
||||
return String(option.label || option.value || '');
|
||||
return this.normalizePathToForward(option.label || option.value || '');
|
||||
}
|
||||
return String(option || '');
|
||||
return this.normalizePathToForward(option || '');
|
||||
},
|
||||
|
||||
getSubfolderOptionBaseDirectory(option) {
|
||||
@@ -1005,10 +1006,11 @@ export const downloadTargetMethods = {
|
||||
joinLocalPath(basePath = '', relativePath = '') {
|
||||
const base = String(basePath || '').replace(/[\/\\]+$/, '');
|
||||
const relative = String(relativePath || '').replace(/^[\/\\]+/, '');
|
||||
if (!base) return relative;
|
||||
if (!base) return this.normalizePathToForward(relative);
|
||||
if (!relative) return base;
|
||||
const separator = base.includes('\\') ? '\\' : '/';
|
||||
return `${base}${separator}${relative}`;
|
||||
const normalizedRelative = relative.replace(/[\/\\]+/g, separator);
|
||||
return `${base}${separator}${normalizedRelative}`;
|
||||
},
|
||||
|
||||
getDownloadTargetFolderContext(category = '', subfolder = '', baseDirectory = '') {
|
||||
@@ -1016,7 +1018,7 @@ export const downloadTargetMethods = {
|
||||
const targetBaseDirectory = baseDirectory || this.getDownloadTargetBaseDirectory(normalizedCategory);
|
||||
if (!targetBaseDirectory) return null;
|
||||
|
||||
const cleanSubfolder = String(subfolder || '').trim();
|
||||
const cleanSubfolder = this.normalizeDownloadSubfolderPath(subfolder);
|
||||
const folderPath = cleanSubfolder
|
||||
? this.joinLocalPath(targetBaseDirectory, cleanSubfolder)
|
||||
: targetBaseDirectory;
|
||||
@@ -1045,7 +1047,7 @@ export const downloadTargetMethods = {
|
||||
syncDownloadTargetFolderContext(categoryEl, subfolderEl) {
|
||||
if (!categoryEl) return;
|
||||
const category = this.normalizeDownloadCategory(this.getDropdownValue(categoryEl) || 'checkpoints');
|
||||
const subfolder = (subfolderEl?.value || '').trim();
|
||||
const subfolder = this.normalizeDownloadSubfolderPath(subfolderEl?.value || '');
|
||||
const subfolderBaseDirectory = subfolderEl?.dataset.baseDirectory || '';
|
||||
|
||||
this.setDownloadFolderContextTarget(
|
||||
@@ -2068,11 +2070,12 @@ export const downloadTargetMethods = {
|
||||
preferTemplate: Boolean(options.preferTemplate)
|
||||
});
|
||||
if (suggestion) {
|
||||
subfolderEl.value = suggestion.value || '';
|
||||
const suggestedSubfolder = this.normalizeDownloadSubfolderPath(suggestion.value || '');
|
||||
subfolderEl.value = suggestedSubfolder;
|
||||
subfolderEl.dataset.baseDirectory = suggestion.baseDirectory || '';
|
||||
this.saveDownloadTargetSelection(missing, {
|
||||
category,
|
||||
subfolder: suggestion.value || '',
|
||||
subfolder: suggestedSubfolder,
|
||||
subfolderBaseDirectory: suggestion.baseDirectory || '',
|
||||
subfolderTouched: false,
|
||||
...this.getSubfolderSuggestionTrackingPatch(suggestion, 'auto')
|
||||
@@ -2109,11 +2112,12 @@ export const downloadTargetMethods = {
|
||||
}
|
||||
|
||||
this.setDropdownValue(categoryEl, category, this.getCategoryDisplayName(category));
|
||||
subfolderEl.value = suggestion.value || '';
|
||||
const suggestedSubfolder = this.normalizeDownloadSubfolderPath(suggestion.value || '');
|
||||
subfolderEl.value = suggestedSubfolder;
|
||||
subfolderEl.dataset.baseDirectory = suggestion.baseDirectory || '';
|
||||
this.saveDownloadTargetSelection(missing, {
|
||||
category,
|
||||
subfolder: suggestion.value || '',
|
||||
subfolder: suggestedSubfolder,
|
||||
subfolderBaseDirectory: suggestion.baseDirectory || '',
|
||||
categoryTouched: Boolean(preserveSavedCategory && category === originalCategory),
|
||||
subfolderTouched: true,
|
||||
@@ -2182,7 +2186,9 @@ export const downloadTargetMethods = {
|
||||
defaultCategory
|
||||
});
|
||||
const preserveSavedSubfolder = Boolean(saved?.subfolderTouched);
|
||||
const selectedSubfolder = preserveSavedSubfolder ? saved.subfolder || '' : '';
|
||||
const selectedSubfolder = preserveSavedSubfolder
|
||||
? this.normalizeDownloadSubfolderPath(saved.subfolder || '')
|
||||
: '';
|
||||
const selectedSubfolderBaseDirectory = preserveSavedSubfolder ? saved.subfolderBaseDirectory || '' : '';
|
||||
const subfolderTooltip = this.getDownloadSubfolderTooltip(missing, selectedCategory, selectedSubfolder, {
|
||||
saved,
|
||||
@@ -2200,7 +2206,7 @@ export const downloadTargetMethods = {
|
||||
html += `<div id="${categoryListId}" class="mr-download-target-list"></div>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="mr-download-target-wrap mr-download-target-subfolder-wrap">`;
|
||||
html += `<input id="${subfolderId}" class="mr-download-target-input" type="text" placeholder="e.g. ponyxl\\styles" autocomplete="off" value="${this.escapeHtml(selectedSubfolder)}" data-base-directory="${this.escapeHtml(selectedSubfolderBaseDirectory)}">`;
|
||||
html += `<input id="${subfolderId}" class="mr-download-target-input" type="text" placeholder="e.g. ponyxl/styles" autocomplete="off" value="${this.escapeHtml(selectedSubfolder)}" data-base-directory="${this.escapeHtml(selectedSubfolderBaseDirectory)}">`;
|
||||
html += `<div id="${subfolderListId}" class="mr-download-target-list"></div>`;
|
||||
html += `</div>`;
|
||||
html += `<button id="${suggestId}" class="mr-btn mr-btn-secondary mr-btn-sm mr-download-suggest-btn" type="button" data-tooltip="Apply suggested subfolder" aria-label="Apply suggested subfolder">${getSvgIcon('lightbulb', 'currentColor', 'mr-download-suggest-icon')}</button>`;
|
||||
@@ -2216,7 +2222,10 @@ export const downloadTargetMethods = {
|
||||
this.getDropdownValue(categoryEl)
|
||||
|| this.getMissingDownloadCategory(missing, fallbackCategory || 'checkpoints')
|
||||
);
|
||||
const subfolder = (subfolderEl?.value || '').trim();
|
||||
const subfolder = this.normalizeDownloadSubfolderPath(subfolderEl?.value || '');
|
||||
if (subfolderEl && subfolderEl.value !== subfolder) {
|
||||
subfolderEl.value = subfolder;
|
||||
}
|
||||
const subfolderBaseDirectory = subfolder ? subfolderEl?.dataset.baseDirectory || '' : '';
|
||||
this.saveDownloadTargetSelection(missing, {
|
||||
category,
|
||||
@@ -2473,11 +2482,11 @@ export const downloadTargetMethods = {
|
||||
});
|
||||
};
|
||||
|
||||
const normalizeSubfolderPath = (value = '') => this.normalizePathToBackward(value)
|
||||
.split('\\')
|
||||
const normalizeSubfolderPath = (value = '') => this.normalizeDownloadSubfolderPath(value)
|
||||
.split('/')
|
||||
.map(part => part.trim())
|
||||
.filter(part => part && part !== '.')
|
||||
.join('\\');
|
||||
.join('/');
|
||||
|
||||
const getFolderBrowserExpandedSet = (category = '') => {
|
||||
if (!this.downloadFolderBrowserExpandedGroups) {
|
||||
@@ -2507,11 +2516,11 @@ export const downloadTargetMethods = {
|
||||
const value = normalizeSubfolderPath(entry.value);
|
||||
if (!value) return;
|
||||
|
||||
const parts = value.split('\\').filter(Boolean);
|
||||
const parts = value.split('/').filter(Boolean);
|
||||
let current = root;
|
||||
let currentPath = '';
|
||||
parts.forEach((part, index) => {
|
||||
currentPath = currentPath ? `${currentPath}\\${part}` : part;
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
if (!current.has(part)) {
|
||||
current.set(part, {
|
||||
name: part,
|
||||
@@ -2561,7 +2570,7 @@ export const downloadTargetMethods = {
|
||||
const shouldExpand = hasChildren && (
|
||||
Boolean(filter)
|
||||
|| expandedSet.has(stateKey)
|
||||
|| selectedValue.toLowerCase().startsWith(`${node.path.toLowerCase()}\\`)
|
||||
|| selectedValue.toLowerCase().startsWith(`${node.path.toLowerCase()}/`)
|
||||
);
|
||||
const toggle = hasChildren
|
||||
? `<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>`
|
||||
@@ -2600,7 +2609,7 @@ export const downloadTargetMethods = {
|
||||
const previousScrollTop = previousScrollEl ? previousScrollEl.scrollTop : 0;
|
||||
const rawFilter = String(filterText || '').trim();
|
||||
const filter = rawFilter.toLowerCase();
|
||||
const normalizedFilter = filter.replace(/[\/]+/g, '\\');
|
||||
const normalizedFilter = filter.replace(/[\\]+/g, '/');
|
||||
const category = this.getDropdownValue(categoryEl);
|
||||
const expandedSet = getFolderBrowserExpandedSet(category);
|
||||
const selectedValue = normalizeSubfolderPath(subfolderEl.value || '');
|
||||
@@ -2839,7 +2848,7 @@ export const downloadTargetMethods = {
|
||||
}
|
||||
this.saveDownloadTargetSelection(missing, {
|
||||
category,
|
||||
subfolder: subfolderEl.value || '',
|
||||
subfolder: this.normalizeDownloadSubfolderPath(subfolderEl.value || ''),
|
||||
subfolderBaseDirectory: subfolderEl.dataset.baseDirectory || '',
|
||||
categoryTouched: true,
|
||||
subfolderTouched: Boolean(subfolderEl.value),
|
||||
@@ -2884,9 +2893,10 @@ export const downloadTargetMethods = {
|
||||
|
||||
subfolderEl.addEventListener('input', () => {
|
||||
subfolderEl.dataset.baseDirectory = '';
|
||||
const normalizedSubfolder = this.normalizeDownloadSubfolderPath(subfolderEl.value);
|
||||
this.saveDownloadTargetSelection(missing, {
|
||||
category: this.getDropdownValue(categoryEl),
|
||||
subfolder: subfolderEl.value,
|
||||
subfolder: normalizedSubfolder,
|
||||
subfolderBaseDirectory: '',
|
||||
subfolderTouched: true,
|
||||
...this.getSubfolderSuggestionTrackingPatch(null)
|
||||
@@ -2895,6 +2905,21 @@ export const downloadTargetMethods = {
|
||||
refreshCategoryTooltip();
|
||||
populateSubfolderOptions(subfolderEl.value);
|
||||
});
|
||||
subfolderEl.addEventListener('blur', () => {
|
||||
const normalizedSubfolder = this.normalizeDownloadSubfolderPath(subfolderEl.value);
|
||||
if (subfolderEl.value !== normalizedSubfolder) {
|
||||
subfolderEl.value = normalizedSubfolder;
|
||||
}
|
||||
this.saveDownloadTargetSelection(missing, {
|
||||
category: this.getDropdownValue(categoryEl),
|
||||
subfolder: normalizedSubfolder,
|
||||
subfolderBaseDirectory: subfolderEl.dataset.baseDirectory || '',
|
||||
subfolderTouched: true,
|
||||
...this.getSubfolderSuggestionTrackingPatch(null)
|
||||
});
|
||||
this.syncDownloadTargetFolderContext(categoryEl, subfolderEl);
|
||||
refreshCategoryTooltip();
|
||||
});
|
||||
|
||||
if (suggestBtn && suggestBtn.dataset.mlSuggestBound !== 'true') {
|
||||
suggestBtn.dataset.mlSuggestBound = 'true';
|
||||
|
||||
@@ -1437,11 +1437,11 @@ export const missingBrowserMethods = {
|
||||
|
||||
const getAllModels = () => Array.isArray(this.allModels) ? this.allModels : [];
|
||||
let localModelLoadToken = 0;
|
||||
const normalizeModelPath = (value = '') => this.normalizePathToBackward(value)
|
||||
.split('\\')
|
||||
const normalizeModelPath = (value = '') => this.normalizePathToForward(value)
|
||||
.split('/')
|
||||
.map(part => part.trim())
|
||||
.filter(Boolean)
|
||||
.join('\\');
|
||||
.join('/');
|
||||
const getModelCategory = (model = {}) => this.normalizeDownloadCategory(model.category || 'unknown') || 'unknown';
|
||||
const getDownloadCategoryElement = () => container.querySelector(`#download-category-${missing.node_id}-${missing.widget_index}`)
|
||||
|| this.contentElement?.querySelector(`#download-category-${missing.node_id}-${missing.widget_index}`);
|
||||
@@ -1537,7 +1537,8 @@ export const missingBrowserMethods = {
|
||||
const relative = String(relativePath || '').replace(/^[\/\\]+/, '');
|
||||
if (!base) return relative;
|
||||
if (!relative) return base;
|
||||
return `${base}${base.includes('\\') ? '\\' : '/'}${relative}`;
|
||||
const separator = base.includes('\\') ? '\\' : '/';
|
||||
return `${base}${separator}${relative.replace(/[\/\\]+/g, separator)}`;
|
||||
};
|
||||
const buildLocalFolderContext = (folderPath = '', name = 'Folder', category = '') => {
|
||||
const path = String(folderPath || '').trim();
|
||||
@@ -1581,9 +1582,9 @@ export const missingBrowserMethods = {
|
||||
const category = getModelCategory(model);
|
||||
const categoryLabel = this.getCategoryDisplayName(category);
|
||||
const relativePath = normalizeModelPath(model.relative_path || model.filename || '');
|
||||
const pathParts = relativePath.split('\\').filter(Boolean);
|
||||
const pathParts = relativePath.split('/').filter(Boolean);
|
||||
const filename = pathParts.pop() || model.filename || relativePath || 'model';
|
||||
const folderPath = pathParts.join('\\');
|
||||
const folderPath = pathParts.join('/');
|
||||
const baseDirectory = String(model.base_directory || '');
|
||||
const fullPath = String(model.path || '');
|
||||
return {
|
||||
@@ -1634,11 +1635,11 @@ export const missingBrowserMethods = {
|
||||
const buildModelTree = (entries = []) => {
|
||||
const root = { folders: new Map(), models: [] };
|
||||
entries.forEach(entry => {
|
||||
const folderParts = entry.folderPath.split('\\').filter(Boolean);
|
||||
const folderParts = entry.folderPath.split('/').filter(Boolean);
|
||||
let current = root;
|
||||
let currentPath = '';
|
||||
folderParts.forEach(part => {
|
||||
currentPath = currentPath ? `${currentPath}\\${part}` : part;
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
if (!current.folders.has(part)) {
|
||||
current.folders.set(part, {
|
||||
name: part,
|
||||
|
||||
Reference in New Issue
Block a user