mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(dashboard): UGC details in the property 3D view and model list
Selecting a brick-built model in the 3D view shows its UGC data under Selected: icons now and before, state, waiting until, UGC id, owner, name and description, bricks, triangles before and after hidden faces were removed, timings, and links to the UGC viewer, NIF and LXFML, with Make again for UGC managers. The property page's model rows have the same block, opened on demand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,75 @@
|
||||
(function () {
|
||||
var STATES = { done: ['Made', 'success'], pending: ['Waiting', 'secondary'], failed: ['Failed', 'danger'] };
|
||||
|
||||
function row(label, value) { return value === '' || value === null || value === undefined ? '' : '<dt class="col-5">' + esc(label) + '</dt><dd class="col-7 text-break">' + value + '</dd>'; }
|
||||
|
||||
// Triangles before and after hidden surfaces were removed, per LOD
|
||||
function triangles(stats) {
|
||||
return (stats.lods || []).map(function (l) {
|
||||
return 'LOD ' + esc(l.lod) + ': ' + esc(l.opaqueBefore) + ' → ' + esc(l.opaqueAfter) + (l.transparent ? ' + ' + esc(l.transparent) + ' transparent' : '') +
|
||||
' <span class="text-body-secondary">(' + esc(l.vertices) + ' vertices)</span>';
|
||||
}).join('<br>');
|
||||
}
|
||||
|
||||
function timings(ms) {
|
||||
if (!ms) return '';
|
||||
var parts = [['build', 'build'], ['hiddenSurfaces', 'hidden faces'], ['ambientOcclusion', 'AO'], ['icon', 'icon']]
|
||||
.filter(function (p) { return ms[p[0]] !== undefined; }).map(function (p) { return esc(p[1]) + ' ' + esc(ms[p[0]]); });
|
||||
return esc(ms.total) + ' ms' + (parts.length ? ' <span class="text-body-secondary">(' + parts.join(', ') + ')</span>' : '');
|
||||
}
|
||||
|
||||
function renderDetails(d, modelId) {
|
||||
var placement = (d.placements || []).find(function (p) { return p.modelId === String(modelId); }) || (d.placements || [])[0];
|
||||
var name = placement && placement.name && !/^Objects_\d+_name$/.test(placement.name) ? placement.name : '';
|
||||
var s = d.stats, prev = d.previousStats;
|
||||
var waiting = d.state === 'pending' && d.processAfter > Date.now() / 1000;
|
||||
var html = '<div class="ugc-details small"><div class="d-flex gap-2 mb-2 align-items-end">' +
|
||||
'<figure class="m-0 text-center"><img src="' + esc(d.icon) + '" width="96" height="96" class="rounded bg-body-tertiary" alt="" onerror="this.style.visibility=\'hidden\'"><figcaption class="text-body-secondary">Now</figcaption></figure>' +
|
||||
'<figure class="m-0 text-center d-none"><img src="' + esc(d.previousIcon) + '" width="64" height="64" class="rounded bg-body-tertiary" alt="" onload="this.parentNode.classList.remove(\'d-none\')"><figcaption class="text-body-secondary">Before · <a href="' + esc(d.previousNif) + '">NIF</a></figcaption></figure></div>' +
|
||||
'<dl class="row mb-2">' +
|
||||
row('State', UgcLinks.state(d) + (d.attempts ? ' <span class="text-body-secondary">' + esc(d.attempts) + ' attempt' + (d.attempts === 1 ? '' : 's') + '</span>' : '') +
|
||||
(d.error ? '<div class="text-danger">' + esc(d.error) + '</div>' : '')) +
|
||||
row('Waiting until', waiting ? esc(fmt.unix(d.processAfter)) + ' <span class="text-body-secondary">(the owner saved it recently)</span>' : '') +
|
||||
row('UGC id', esc(d.ugcId)) +
|
||||
row('Owner', (d.characterName ? fmt.character(d.characterId, d.characterName) : esc(d.characterId)) + (d.accountName ? ' <span class="text-body-secondary">(' + esc(d.accountName) + ')</span>' : '')) +
|
||||
row('Name', name ? esc(name) : '') +
|
||||
row('Description', placement && placement.description ? esc(placement.description) : '') +
|
||||
row('File', d.fileName ? esc(d.fileName) : '') +
|
||||
row('Made', d.processedAt ? esc(fmt.unix(d.processedAt)) : '') +
|
||||
row('Baked AO', d.bakeAo === undefined ? '' : (d.bakeAo ? 'Yes' : 'No')) +
|
||||
row('Bricks', s ? esc(s.bricks) + (prev && prev.bricks !== s.bricks ? ' <span class="text-body-secondary">(was ' + esc(prev.bricks) + ')</span>' : '') : '') +
|
||||
row('Triangles', s ? triangles(s) : '') +
|
||||
row('Time', s ? timings(s.ms) : '') +
|
||||
row('Before', prev ? esc(prev.bricks) + ' bricks<br>' + triangles(prev) : '') +
|
||||
'</dl>';
|
||||
if (d.state === 'done' && !s) html += '<div class="text-body-secondary mb-2">No stats (made before the UGC server wrote them).</div>';
|
||||
var buttons = '';
|
||||
if (d.link) buttons += '<a class="btn btn-sm btn-outline-primary" href="' + esc(d.link) + '">Open in UGC viewer</a>';
|
||||
if (d.canManage) buttons += '<button type="button" class="btn btn-sm btn-outline-warning" data-ugc-remake="' + esc(d.ugcId) + '">Make again</button>';
|
||||
if (d.state === 'done') buttons += '<a class="btn btn-sm btn-outline-secondary" href="' + esc(d.nif) + '">Download NIF</a>';
|
||||
if (modelId) buttons += '<a class="btn btn-sm btn-outline-secondary" href="/api/property_models/' + esc(modelId) + '/lxfml">Download LXFML</a>';
|
||||
return html + (buttons ? '<div class="d-flex flex-wrap gap-1">' + buttons + '</div>' : '') + '</div>';
|
||||
}
|
||||
|
||||
// "Make again" in any UGC block
|
||||
document.addEventListener('click', function (e) {
|
||||
var button = e.target.closest && e.target.closest('[data-ugc-remake]');
|
||||
if (!button) return;
|
||||
button.disabled = true;
|
||||
api.post('/api/ugc/reprocess', { kind: 'model', id: button.dataset.ugcRemake }).then(function (d) {
|
||||
toast(d.success ? 'The UGC server will make it again' : (d.error || 'Failed'), d.success ? 'success' : 'danger');
|
||||
}).catch(function () { toast('Failed', 'danger'); }).then(function () { button.disabled = false; });
|
||||
});
|
||||
|
||||
// Expandable blocks (<details data-ugc-details="ugcId" data-model="modelId" data-via="property=...">) load when first opened
|
||||
document.addEventListener('toggle', function (e) {
|
||||
var box = e.target;
|
||||
if (!box.matches || !box.matches('details[data-ugc-details]') || !box.open || box.dataset.loaded) return;
|
||||
box.dataset.loaded = '1';
|
||||
var body = box.querySelector('.ugc-details-body');
|
||||
UgcLinks.details(box.dataset.ugcDetails, box.dataset.via, box.dataset.model).then(function (html) { body.innerHTML = html; });
|
||||
}, true);
|
||||
|
||||
window.UgcLinks = {
|
||||
// A badge for the UGC server's state; a failure's reason as its tooltip
|
||||
state: function (item) {
|
||||
@@ -18,7 +87,19 @@
|
||||
return '<img src="' + esc(item.icon) + '" width="' + size + '" height="' + size + '" class="rounded bg-body-tertiary" alt="" loading="lazy" ' +
|
||||
'onerror="this.onerror=null;this.src=\'/api/icon/' + esc(lot) + '\'">';
|
||||
},
|
||||
// {modelId: item} for the player-built models on a property
|
||||
/**
|
||||
* The UGC block for a brick-built model on the property pages: its icons (and the previous version's), state,
|
||||
* owner, the name the player gave it, the UGC server's stats and links. `via` is the page's ?property= / ?character=
|
||||
* query that lets its viewer see it; `modelId` (a placed model) picks its name and LXFML download.
|
||||
*/
|
||||
details: function (ugcId, via, modelId) {
|
||||
if (!ugcId || ugcId === '0') return Promise.resolve('<div class="small text-body-secondary">No UGC data: this model has no blueprint.</div>');
|
||||
return api.get('/api/ugc_links/model/' + encodeURIComponent(ugcId) + (via ? '?' + via : '')).then(function (d) {
|
||||
if (!d || d.success === false) return '<div class="small text-body-secondary">' + esc((d && d.error) || 'No UGC data for this model') + '</div>';
|
||||
return renderDetails(d, modelId);
|
||||
}).catch(function (e) { return '<div class="small text-body-secondary">' + esc((e && e.message) || 'Could not load the UGC data') + '</div>'; });
|
||||
},
|
||||
// {modelId: item} for the player-built models on a property // {modelId: item} for the player-built models on a property
|
||||
forProperty: function (propertyId) {
|
||||
return api.get('/api/ugc_links/property/' + propertyId).then(function (d) {
|
||||
var map = {};
|
||||
|
||||
Reference in New Issue
Block a user