Files
DemonGatanjieuandClaude Opus 5.5 b261968e79 Show the versions of one model together and switch between them
Files a scan found on the same Civitai model page (metadata model_id; never a similar
file name) share one card in the model list, the newest version shown or the one picked
last (kept in this browser). The card's version label opens the shared action menu to
switch. The details list the versions here under the name, and "All versions on Civitai"
(GET /anomalous/download/versions, this computer only, asked only when pressed) lists
the page's versions to open or download beside this one, checked by SHA-256, a taken
file name getting the version's name added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 04:44:10 +08:00

76 lines
3.2 KiB
JavaScript

/**
* Versions of one model (pure): files whose scan found the same Civitai model page (metadata.model_id)
* are versions of one model, told apart by their version id. Only that id groups them, never a
* similar file name: two files of one name may be unrelated, and versions often share a name.
* Files not matched on Civitai stay on their own. ui_grid.js shows a group as one card with a
* version switch, ui_model_versions.js draws the switch and the details' version row. freeName
* names a version's download when its file name is taken (a download never replaces a file).
*/
const positive = value => {
const number = Number(value);
return Number.isInteger(number) && number > 0 ? number : 0;
};
/** The model's Civitai page id, or 0 when it has none. */
export const pageId = model => positive(model?.metadata?.model_id);
/** The model's Civitai version id, or 0. */
export const versionId = model => positive(model?.metadata?.version_id);
/** Newest first: Civitai's version ids grow with time; a file without one goes last, then by file name. */
function newestFirst(a, b) {
return (versionId(b) - versionId(a)) || String(a.filename).localeCompare(String(b.filename));
}
/**
* `models` as the list shows them: one entry a file, or a model page's files together, at the place
* of the first of them. Each entry is { model, versions }: `versions` newest first (only the model
* itself when it has no others), `model` the version `chosen(pageId)` names when it is among them,
* else the newest.
*/
export function groupByPage(models, chosen = () => 0) {
const entries = [];
const byPage = new Map();
for (const model of models || []) {
const page = pageId(model);
if (!page) {
entries.push({ model, versions: [model] });
continue;
}
let entry = byPage.get(page);
if (!entry) {
entry = { model, versions: [] };
byPage.set(page, entry);
entries.push(entry);
}
entry.versions.push(model);
}
for (const [page, entry] of byPage) {
entry.versions.sort(newestFirst);
const wanted = chosen(page);
entry.model = entry.versions.find(model => wanted && versionId(model) === wanted) || entry.versions[0];
}
return entries;
}
/** The versions of `model` among `models` (itself included), newest first; just itself when it has no page. */
export function versionsOf(models, model) {
const page = pageId(model);
if (!page) return [model];
const found = (models || []).filter(other => pageId(other) === page);
if (!found.includes(model)) found.push(model);
return found.sort(newestFirst);
}
/** `name` with `version` added before its extension, when a file of that name is here already. */
export function freeName(name, version, taken) {
if (!taken.has(name.toLowerCase())) return name;
const dot = name.lastIndexOf('.');
const [stem, ext] = dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, ''];
const tag = String(version || '').replace(/[^\p{L}\p{N}._-]+/gu, '_').replace(/^_+|_+$/g, '') || 'version';
let candidate = `${stem}_${tag}${ext}`;
for (let n = 2; taken.has(candidate.toLowerCase()); n++) candidate = `${stem}_${tag}_${n}${ext}`;
return candidate;
}