mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-15 17:14:07 +00:00
feat(dashboard): UGC search, and UGC icons on property and character pages
UGC Search (/ugc_search) finds creations by name, id or LOT with where each one is. Property pages and character inventories show the icon the UGC server made of each creation, its state and a link to /ugc?item=&kind=, for whoever may view the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
60
dDashboardServer/static/js/ugc-links.js
Normal file
60
dDashboardServer/static/js/ugc-links.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Players' creations on the property and character pages: the icon the UGC server made of a model, car or rocket in
|
||||
* place of the item's own icon, its state and a link to it on the /ugc page (for those who may open it). The routes
|
||||
* (/api/ugc_links/...) let whoever may view the page see the icons of the creations on it.
|
||||
*/
|
||||
(function () {
|
||||
var STATES = { done: ['Made', 'success'], pending: ['Waiting', 'secondary'], failed: ['Failed', 'danger'] };
|
||||
|
||||
window.UgcLinks = {
|
||||
// A badge for the UGC server's state; a failure's reason as its tooltip
|
||||
state: function (item) {
|
||||
var s = STATES[item.state] || [item.state, 'secondary'];
|
||||
return '<span class="badge text-bg-' + s[1] + '"' + (item.error ? ' title="' + esc(item.error) + '"' : '') + '>' + esc(s[0]) + '</span>';
|
||||
},
|
||||
// An <img> of the creation's icon that falls back to the item's own icon until the UGC server made one
|
||||
icon: function (item, lot, size) {
|
||||
size = size || 40;
|
||||
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
|
||||
forProperty: function (propertyId) {
|
||||
return api.get('/api/ugc_links/property/' + propertyId).then(function (d) {
|
||||
var map = {};
|
||||
((d && d.items) || []).forEach(function (item) { map[item.modelId] = item; });
|
||||
return map;
|
||||
}).catch(function () { return {}; });
|
||||
},
|
||||
// Shows the creations among a character page's items (.inv-item[data-id]) with their own icon and a link
|
||||
decorateInventory: function (characterId, root) {
|
||||
return api.get('/api/ugc_links/character/' + characterId).then(function (d) {
|
||||
((d && d.items) || []).forEach(function (item) {
|
||||
(root || document).querySelectorAll('.inv-item[data-id="' + item.itemId + '"]').forEach(function (el) {
|
||||
var img = el.querySelector('img');
|
||||
if (img) {
|
||||
var own = img.src;
|
||||
img.onerror = function () { img.onerror = null; img.src = own; };
|
||||
img.src = item.icon;
|
||||
}
|
||||
el.classList.add('inv-ugc');
|
||||
el.title = (el.title ? el.title + ' - ' : '') + 'Player-built ' + (item.kind === 'modular' ? 'car or rocket' : 'model') + ' (UGC ' + item.ugcId + ', ' + item.state + ')';
|
||||
if (item.link && !el.querySelector('.inv-ugc-link')) {
|
||||
// Items may sit inside a link already (the item trace), so this is a button, not a nested link
|
||||
var a = document.createElement('span');
|
||||
a.className = 'inv-ugc-link';
|
||||
a.setAttribute('role', 'link');
|
||||
a.tabIndex = 0;
|
||||
a.textContent = 'UGC';
|
||||
a.title = 'Open in the UGC viewer';
|
||||
var open = function (e) { e.preventDefault(); e.stopPropagation(); window.location.href = item.link; };
|
||||
a.addEventListener('click', open);
|
||||
a.addEventListener('keydown', function (e) { if (e.key === 'Enter') open(e); });
|
||||
el.appendChild(a);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).catch(function () {});
|
||||
}
|
||||
};
|
||||
})();
|
||||
66
dDashboardServer/static/js/ugc-search.js
Normal file
66
dDashboardServer/static/js/ugc-search.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* The UGC search (/ugc_search): players' creations by name, id or LOT and where each one is (/api/ugc_links/search).
|
||||
* ?q= fills the search in; the address keeps the last search so it can be shared.
|
||||
*/
|
||||
(function () {
|
||||
var input = document.getElementById('ugcSearchInput');
|
||||
var rows = document.getElementById('ugcSearchRows');
|
||||
var status = document.getElementById('ugcSearchStatus');
|
||||
var latest = 0, timer = null;
|
||||
var INVENTORIES = { 5: 'Models', 12: 'Vault models', 14: 'Brick-building models' };
|
||||
|
||||
function inventoryName(type) { return (window.Labels && Labels.name('inventories', type)) || INVENTORIES[type] || 'Inventory ' + type; }
|
||||
|
||||
function where(item) {
|
||||
if (!item.where.length) {
|
||||
return '<span class="text-body-secondary small" title="Not placed on a property, not in the mail and not in its creator\'s inventories: traded, sold or deleted">Not found</span>';
|
||||
}
|
||||
return item.where.map(function (w) {
|
||||
if (w.type === 'property') {
|
||||
// A model the player didn't name has the client's placeholder (Objects_<lot>_name)
|
||||
var given = w.modelName && !/^Objects_\d+_name$/.test(w.modelName);
|
||||
var name = given ? ' as <span class="text-break">' + esc(w.modelName) + '</span>' : '';
|
||||
return '<div>On ' + fmt.link('/properties/' + w.propertyId, w.propertyName || 'Property ' + w.propertyId) + name +
|
||||
' <span class="small text-body-secondary">(' + fmt.character(w.ownerId, w.ownerName) + ', ' + esc(w.zoneId) + ')</span>' +
|
||||
' <a class="small" href="/properties/' + esc(w.propertyId) + '/3d#model=' + esc(w.modelId) + '">3D</a></div>';
|
||||
}
|
||||
if (w.type === 'mail') return '<div>In the mail to ' + fmt.character(w.characterId, w.characterName) + '</div>';
|
||||
return '<div>In ' + fmt.character(w.characterId, item.characterName) + '\'s ' + esc(inventoryName(w.inventory)) + '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function render(items) {
|
||||
rows.innerHTML = items.length ? items.map(function (item) {
|
||||
var lot = item.kind === 'modular' ? 6416 : 6662;
|
||||
var label = item.kind === 'modular' ? 'Car or rocket' : 'Model';
|
||||
var title = item.link ? '<a href="' + esc(item.link) + '">' + label + ' ' + esc(item.ugcId) + '</a>' : label + ' ' + esc(item.ugcId);
|
||||
var detail = item.detail ? '<div class="small text-body-secondary text-break">' + (item.kind === 'modular' ? 'Modules <code>' + esc(item.detail) + '</code>' : esc(item.detail)) + '</div>' : '';
|
||||
var account = item.accountName ? '<div class="small text-body-secondary">' + (document.getElementById('ugcSearchPage').dataset.canAccounts ? fmt.link('/accounts/' + item.accountId, item.accountName) : esc(item.accountName)) + '</div>' : '';
|
||||
return '<tr><td data-label="">' + UgcLinks.icon(item, lot, 48) + '</td><td data-label="Creation">' + title + detail + '</td>' +
|
||||
'<td data-label="Made by">' + fmt.character(item.characterId, item.characterName) + account + '</td>' +
|
||||
'<td data-label="Where">' + where(item) + '</td><td data-label="UGC server">' + UgcLinks.state(item) + '</td></tr>';
|
||||
}).join('') : '<tr><td colspan="5" class="text-body-secondary">Nothing matches.</td></tr>';
|
||||
}
|
||||
|
||||
function search() {
|
||||
var q = input.value.trim();
|
||||
var request = ++latest;
|
||||
var url = new URL(window.location.href);
|
||||
if (q) url.searchParams.set('q', q); else url.searchParams.delete('q');
|
||||
history.replaceState(null, '', url);
|
||||
if (!q) { rows.innerHTML = '<tr><td colspan="5" class="text-body-secondary">Type something to search.</td></tr>'; status.textContent = ''; return; }
|
||||
status.textContent = 'Searching…';
|
||||
api.get('/api/ugc_links/search?q=' + encodeURIComponent(q)).then(function (d) {
|
||||
if (request !== latest) return;
|
||||
var items = (d && d.items) || [];
|
||||
status.textContent = items.length + ' found' + (items.length >= 50 ? ' (the newest 50 of each kind; narrow the search for more)' : '');
|
||||
render(items);
|
||||
}).catch(function () { if (request === latest) status.textContent = 'The search failed.'; });
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(search, 300); });
|
||||
document.getElementById('ugcSearchForm').addEventListener('submit', function (e) { e.preventDefault(); clearTimeout(timer); search(); });
|
||||
input.value = document.getElementById('ugcSearchPage').dataset.query || '';
|
||||
if (input.value) search();
|
||||
input.focus();
|
||||
})();
|
||||
Reference in New Issue
Block a user