// sortof-shared - D proxy, pure helpers, and leaf components
// Loaded before sortof-app.jsx; defines globals that App and extracted components reference.
window._liveSortData = null;
const D = new Proxy({}, {
get(_, key) {
const src = window._liveSortData ?? window.SORTOF_DATA;
return src ? src[key] : undefined;
}
});
// Make globally visible for sortof-app.jsx (loaded as separate script).
Object.assign(window, { D, _liveSortData: null, parseWorkshopInput, buildModsLine,
_applyAppendWsid, _applyStripWsid, _applyEnsureAdded, _applyEnsureRemoved, _applyEnsureSwapped,
pollJobOnce, pollJobLoop, isRadioMode, defaultSelectionForBranches, fullSelectionForWsid,
COLUMN_COUNT, wsidUrl, WsidLink, IconCopy, IconCheck, IconGit, IconLink,
CopyBtn, ShareBtn, rowStateForMod, WarnMsg, warnAccent, actionStaged, unknownRemovalWarnings,
failedIndexWarnings });
function parseWorkshopInput(text) {
if (!text) return [];
const cleaned = String(text).replace(/^\s*(WorkshopItems|Mods|Map)\s*=\s*/gim, '');
const ids = cleaned.match(/\b\d{7,12}\b/g) || [];
return Array.from(new Set(ids));
}
function buildModsLine(ids, mode) {
if (!ids || !ids.length) return '';
if (mode === 'B42') return '\\' + ids.join(';\\');
return ids.join(';');
}
function _applyAppendWsid(input, wsid) {
const trimmed = (input || '').replace(/\s+$/, '');
const sep = trimmed ? '\n' : '';
return trimmed + sep + wsid;
}
function _applyStripWsid(input, wsid) {
return (input || '').replace(
new RegExp(`(?:^|(?<=[\\s;,]))${wsid}(?=$|[\\s;,])`, 'g'), ''
).replace(/[\s;,]{2,}/g, m => m.includes('\n') ? '\n' : m[0]).trim();
}
function _applyEnsureAdded(input, wsid) {
const wid = String(wsid);
return parseWorkshopInput(input).includes(wid) ? input : _applyAppendWsid(input, wid);
}
function _applyEnsureRemoved(input, wsid) {
const w = String(wsid);
return parseWorkshopInput(input).includes(w) ? _applyStripWsid(input, w) : input;
}
function _applyEnsureSwapped(input, from, to) {
const f = String(from);
const t = String(to);
const existing = parseWorkshopInput(input);
if (!existing.includes(f)) return input;
if (existing.includes(t)) return _applyStripWsid(input, f);
return input.replace(new RegExp(`(?<=^|[\\s;,])${f}(?=$|[\\s;,])`, 'g'), t);
}
const POLL_INTERVAL_MS = 2500;
async function pollJobOnce(jobId) {
try {
const res = await fetch(`/api/jobs/${jobId}`);
if (res.status === 404) return { kind: 'expired' };
if (!res.ok) return { kind: 'error', status: res.status };
const json = await res.json();
return { kind: 'ok', body: json };
} catch (e) {
console.warn('pollJobOnce: network error', e);
return { kind: 'error', status: 0 };
}
}
function pollJobLoop(jobId, signal, onTick) {
return new Promise((resolve) => {
let timer = null;
async function tick() {
if (signal.aborted) { if (timer) clearTimeout(timer); resolve({ kind: 'aborted' }); return; }
const r = await pollJobOnce(jobId);
if (signal.aborted) { resolve({ kind: 'aborted' }); return; }
onTick(r);
if (r.kind === 'expired' || r.kind === 'error') { resolve(r); return; }
const phase = r.body.phase;
if (phase === 'done' || phase === 'failed') { resolve(r); return; }
timer = setTimeout(tick, POLL_INTERVAL_MS);
}
tick();
});
}
function isRadioMode(branches) {
if (!branches || branches.length < 2) return false;
const ids = new Set(branches.map(b => b.modId));
return branches.some(b => (b.conflicts || []).some(c => ids.has(c)));
}
function defaultSelectionForBranches(branches, activeSet) {
if (!branches || !branches.length) return [];
if (activeSet && activeSet.size) {
const active = branches.filter(b => activeSet.has(b.modId)).map(b => b.modId);
if (active.length) return active;
}
return [branches[0].modId];
}
function fullSelectionForWsid(wsid, selections) {
if (selections && selections[wsid] !== undefined) return selections[wsid];
const active = new Set(D.SORTED_ORDER || []);
return (D.MOD_DB || [])
.filter(m => String(m.wsid) === String(wsid) && active.has(m.modId))
.map(m => m.modId);
}
const COLUMN_COUNT = 6;
function wsidUrl(wsid) {
return `https://steamcommunity.com/sharedfiles/filedetails/?id=${wsid}`;
}
function WsidLink({ wsid, children, className }) {
if (!wsid) return {children ?? wsid};
return {children ?? wsid};
}
function IconCopy() { return ; }
function IconCheck() { return ; }
function IconGit() { return ; }
function IconLink() { return ; }
function CopyBtn({ text, label, minimal }) {
const [copied, setCopied] = React.useState(false);
const doCopy = async () => {
try { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 1800); }
catch { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px';
document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta);
setCopied(true); setTimeout(() => setCopied(false), 1800);
}
};
if (minimal) return ;
return ;
}
function ShareBtn({ jobId }) {
const [saving, setSaving] = React.useState(false);
const [code, setCode] = React.useState(null);
if (!jobId) return null;
const onClick = async () => {
setSaving(true); try {
const res = await fetch(`/api/jobs/${jobId}/share`, { method: 'POST' });
if (res.ok) { const json = await res.json(); setCode(json.code); } else { setCode('error'); }
} catch { setCode('error'); } setSaving(false);
};
if (code) return sortof.indifferentketchup.com/s/{code}