Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_folder_list.js
T
DemonGatanjieuandClaude Opus 5.5 e0abbd277a Folder column: the current type's folders, with 📂 to open one in Explorer
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>
2026-10-13 20:28:03 +08:00

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;
}