The list column showed every type's tree (raw folder names, [Root] rows with 0, nothing marking where you are), repeating the type chips above it. Now (ui_folder_list.js) it follows the chips: the type's heading with "📂 Open", All, its subfolders as a tree whose numbers count their subfolders (empty ones dimmed, folding per session in owner.collapsedFolders), and "In the top folder" for models directly in the type's folder; the listed one is marked. A Bilibili comment asked for quick ways to open model folders: POST /anomalous/open_model_folder without a file name now opens the folder itself (type root or subfolder; still by type, index and subfolder, never a path, local only). Tests in tests/test_model_folder.py. Gone: the every-type tree, Expand / Collapse all, expandedFolders and their locale keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
4.5 KiB
JavaScript
99 lines
4.5 KiB
JavaScript
/**
|
|
* The models page's folder column: the folders of the type the chips show, not every type. Its
|
|
* heading names the type with 📂 Open (Explorer opens the type's folder); then All (the whole
|
|
* type), the subfolders as a tree whose numbers count their subfolders too (empty ones dimmed,
|
|
* ▸ folds one away for this session), and "In the top folder" for models lying directly in the
|
|
* type's folder when it has subfolders. The listed folder is highlighted; a subfolder's 📂
|
|
* shows on hover.
|
|
*/
|
|
|
|
import { translate as t } from './locales.js';
|
|
import { openType, typeLabel } from './ui_model_types.js';
|
|
import { openModelFolder } from './model_location.js';
|
|
|
|
function el(tag, className, text) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
function openButton(where, label = '') {
|
|
const button = el('button', 'anomalous-folder-open', `📂${label ? ` ${label}` : ''}`);
|
|
button.type = 'button';
|
|
button.title = t('folderOpenHint');
|
|
button.onclick = (event) => {
|
|
event.stopPropagation();
|
|
openModelFolder(where);
|
|
};
|
|
return button;
|
|
}
|
|
|
|
function listFolder(owner, group, subfolder) {
|
|
owner.modelScope = { type: group.type, path_idx: group.path_idx, subfolder };
|
|
owner.modelView = '';
|
|
owner.currentType = group.type;
|
|
owner.currentPathIdx = group.path_idx;
|
|
owner.currentSubfolder = subfolder;
|
|
owner.hideAllPanels();
|
|
owner.grid.style.display = 'grid';
|
|
owner.renderSidebar();
|
|
owner.loadModels();
|
|
}
|
|
|
|
export function renderFolderColumn(owner) {
|
|
const box = el('div', 'anomalous-folder-column');
|
|
const scope = owner.modelScope;
|
|
const group = (owner.foldersData || []).find(item => item.type === scope?.type && item.path_idx === scope?.path_idx);
|
|
if (!group) return box;
|
|
const where = (subfolder) => ({ type: group.type, path_idx: group.path_idx, subfolder });
|
|
const folders = group.folders || {};
|
|
const paths = Object.keys(folders);
|
|
const subfolders = paths.filter(path => path !== '/').sort((a, b) => a.localeCompare(b));
|
|
const count = (path) => paths
|
|
.filter(item => path === '/' || item === path || item.startsWith(`${path}/`))
|
|
.reduce((sum, item) => sum + (folders[item].model_count || 0), 0);
|
|
const folded = owner.collapsedFolders;
|
|
|
|
const row = ({ label, number, depth, active, onPick, path, toggle }) => {
|
|
const item = el('div', 'anomalous-folder-item');
|
|
item.style.paddingLeft = `${12 + depth * 14}px`;
|
|
item.classList.toggle('active', active);
|
|
item.classList.toggle('is-empty', !number);
|
|
const fold = el('span', 'anomalous-folder-toggle', toggle ? (folded.has(toggle) ? '▸' : '▾') : '');
|
|
if (toggle) {
|
|
fold.onclick = (event) => {
|
|
event.stopPropagation();
|
|
if (folded.has(toggle)) folded.delete(toggle);
|
|
else folded.add(toggle);
|
|
owner.renderSidebar();
|
|
};
|
|
}
|
|
const name = el('span', 'anomalous-folder-name', label);
|
|
name.title = label;
|
|
item.append(fold, name, el('span', 'anomalous-folder-count', String(number)));
|
|
if (path) item.append(openButton(where(path)));
|
|
item.onclick = onPick;
|
|
return item;
|
|
};
|
|
|
|
const head = el('div', 'anomalous-folder-head');
|
|
head.append(el('span', 'anomalous-folder-head-name', typeLabel(group)), openButton(where('/'), t('folderOpen')));
|
|
box.append(head, row({ label: t('folderAll'), number: count('/'), depth: 0, active: !scope.subfolder, onPick: () => openType(owner, group) }));
|
|
for (const path of subfolders) {
|
|
const parts = path.split('/').filter(Boolean);
|
|
const hidden = parts.slice(0, -1).some((_, index) => folded.has(`${group.type}/${parts.slice(0, index + 1).join('/')}`));
|
|
if (hidden) continue;
|
|
const hasChildren = subfolders.some(item => item.startsWith(`${path}/`));
|
|
box.append(row({
|
|
label: folders[path].name, number: count(path), depth: parts.length, path,
|
|
active: scope.subfolder === path, toggle: hasChildren ? group.type + path : '',
|
|
onPick: () => listFolder(owner, group, path),
|
|
}));
|
|
}
|
|
if (subfolders.length && folders['/']?.model_count) {
|
|
box.append(row({ label: t('folderTopOnly'), number: folders['/'].model_count, depth: 0, path: '/', active: scope.subfolder === '/', onPick: () => listFolder(owner, group, '/') }));
|
|
}
|
|
return box;
|
|
}
|