mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
fmt.property(id, name) links a property's page and adds a small 3D button straight to its 3D view. Used everywhere a property is linked: the properties list and top slots, moderation, character pages' related properties, UGC search and the UGC page's placements, the home page, the chat log, player reports and report notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
169 lines
10 KiB
Django/Jinja
169 lines
10 KiB
Django/Jinja
{% extends "base.jinja2" %}
|
|
|
|
{% block title %}Properties - DarkflameServer{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h2 class="mb-1">Properties</h2>
|
|
<p class="text-body-secondary mb-0">Player properties — <span id="propertyCount">0</span> shown</p>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="pendingOnly" data-pref="properties.unapprovedOnly">
|
|
<label class="form-check-label" for="pendingOnly">Awaiting review only</label>
|
|
</div>
|
|
</div>
|
|
|
|
{% if can.feature_properties %}
|
|
<div class="card mb-4" id="topPropertiesCard">
|
|
<div class="card-header d-flex flex-wrap justify-content-between align-items-start gap-2">
|
|
<div>
|
|
<h5 class="mb-0"><button type="button" class="btn btn-link p-0 text-body text-decoration-none fs-5 dropdown-toggle" data-bs-toggle="collapse" data-bs-target="#topPropertiesBody" aria-expanded="true" aria-controls="topPropertiesBody">Today's Top Properties</button></h5>
|
|
<details class="about-text"><summary>How the slots are picked</summary><div>The four slots of the game's news screen. Each slot belongs to one property world and can only show a property of that world, since the game names the slot's world whatever property is in it. Only approved public properties can be shown, and never one in two slots. <em>Full auto</em> shows each world's property with the most reputation; <em>Per slot</em> lets each slot show its world's <em>Auto</em> pick (the most reputation not shown in another slot), a picked one, or nothing. Players see a change the next time they open the news screen (within 30 seconds).</div></details>
|
|
<div class="small text-body-secondary" id="topPropertiesSetBy"></div>
|
|
</div>
|
|
<div class="btn-group btn-group-sm flex-shrink-0" role="group" aria-label="Top properties mode">
|
|
<input type="radio" class="btn-check" name="topPropertiesMode" id="topPropertiesPerSlot" value="per_slot" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="topPropertiesPerSlot">Per slot</label>
|
|
<input type="radio" class="btn-check" name="topPropertiesMode" id="topPropertiesFullAuto" value="full_auto" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="topPropertiesFullAuto">Full auto</label>
|
|
</div>
|
|
</div>
|
|
<div class="card-body collapse show" id="topPropertiesBody" data-pref-open="properties.topOpen">
|
|
<div class="row g-3" id="topPropertySlots"><div class="col text-body-secondary">Loading…</div></div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<table id="propertiesTable" class="table table-striped table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Name</th>
|
|
<th>Owner</th>
|
|
<th>Moderation</th>
|
|
<th>Reputation</th>
|
|
<th title="Load estimate of the placed models, reported by the owner's game client">Performance cost</th>
|
|
<th>Zone</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
$(function () {
|
|
var pending = document.getElementById('pendingOnly');
|
|
var table = serverTable('#propertiesTable', '/api/tables/properties', [
|
|
{ data: 'id' },
|
|
{ data: 'name', render: function (d, t, row) { return t === 'display' ? fmt.property(row.id, d) : d; } },
|
|
{ data: 'owner_id', render: function (d, t, row) { return fmt.character(d, row.owner_name); } },
|
|
{ data: 'mod_approved', render: function (d) { return d ? fmt.badge('Approved', 'success') : fmt.badge('Unapproved', 'warning'); } },
|
|
{ data: 'reputation' },
|
|
{ data: 'performance_cost', render: function (d, t) { return t === 'display' ? (Number(d) ? Number(d).toFixed(1) : '<span class="text-body-secondary">-</span>') : d; } },
|
|
{ data: 'zone_id', render: function (d, t, row) { return fmt.zone(d, row.zone_name); } },
|
|
{ data: null, orderable: false, searchable: false, render: function (d) {
|
|
if (d.mod_approved || !DASH.can('moderate_properties')) return '<span class="text-body-secondary">-</span>';
|
|
return '<button class="btn btn-sm btn-outline-success" data-approve="' + esc(d.id) + '">Approve</button>';
|
|
}}
|
|
], { liveTable: 'properties', extra: function () { return { pending: pending.checked }; } });
|
|
table.on('draw.dt', function () { document.getElementById('propertyCount').textContent = table.page.info().recordsDisplay; });
|
|
pending.addEventListener('change', function () { table.ajax.reload(); });
|
|
|
|
$('#propertiesTable').on('click', '[data-approve]', function () {
|
|
if (!confirm('Approve this property?')) return;
|
|
api.action('/api/properties/' + this.getAttribute('data-approve') + '/approve', {}, 'Property approved').catch(function () {});
|
|
});
|
|
|
|
if (DASH.can('feature_properties')) {
|
|
var slotsEl = document.getElementById('topPropertySlots');
|
|
var modeInputs = document.querySelectorAll('input[name="topPropertiesMode"]');
|
|
var propLabel = function (p) { return (p.name || '(unnamed)') + ' by ' + (p.owner_name || '?') + ' \u2014 ' + Number(p.reputation || 0).toLocaleString() + ' rep'; };
|
|
var countLabel = function (n) { return n + ' approved public ' + (n === 1 ? 'property' : 'properties'); };
|
|
var renderSlots = function (state) {
|
|
var slots = state.slots || [];
|
|
var fullAuto = !!state.full_auto;
|
|
modeInputs.forEach(function (el) { el.checked = (el.value === 'full_auto') === fullAuto; el.disabled = false; });
|
|
document.getElementById('topPropertiesSetBy').textContent = state.settings_updated_at
|
|
? 'Switched to ' + (fullAuto ? 'full auto' : 'per slot') + ' by ' + (state.settings_updated_by || '?') + ' ' + new Date(state.settings_updated_at * 1000).toLocaleString() : '';
|
|
if (!slots.length) { slotsEl.innerHTML = '<div class="col text-body-secondary">No property worlds found in the client data.</div>'; return; }
|
|
slotsEl.innerHTML = slots.map(function (s) {
|
|
var showing = s.showing
|
|
? fmt.property(s.showing.id, s.showing.name) + ' <span class="text-body-secondary">by ' + esc(s.showing.owner_name) + '</span>'
|
|
: '<span class="text-body-secondary">Nothing (locked slot)</span>';
|
|
var warn = fullAuto ? '' : s.picked_unavailable ? '<div class="small text-warning mt-1">The picked property isn\'t an approved public property of ' + esc(s.zone_name) + ' (any more), so the slot falls back to Auto.</div>'
|
|
: s.picked_shown_elsewhere ? '<div class="small text-warning mt-1">The picked property is already shown in an earlier slot, so this one falls back to Auto.</div>' : '';
|
|
var changed = s.updated_at ? '<div class="small text-body-secondary mt-1">Set by ' + esc(s.updated_by || '?') + ' ' + esc(new Date(s.updated_at * 1000).toLocaleString()) + '</div>' : '';
|
|
var controls = fullAuto ? '<div class="small text-body-secondary mb-2">Full auto is on: this slot\'s own setting (' + esc(s.mode) + ') is kept for when you switch back to per slot.</div>' :
|
|
'<div class="small text-body-secondary mb-2"><span data-count>' + countLabel(s.candidate_total) + '</span></div>' +
|
|
'<select class="form-select form-select-sm mb-2" data-mode aria-label="What the slot shows">' +
|
|
'<option value="auto"' + (s.mode === 'auto' ? ' selected' : '') + '>Auto (most reputation)</option>' +
|
|
'<option value="picked"' + (s.mode === 'picked' ? ' selected' : '') + (s.candidate_total || s.picked ? '' : ' disabled') + '>Pick a property</option>' +
|
|
'<option value="empty"' + (s.mode === 'empty' ? ' selected' : '') + '>Empty</option>' +
|
|
'</select>' +
|
|
'<div class="mb-2" data-property-box' + (s.mode === 'picked' ? '' : ' hidden') + '><input type="search" class="form-control form-control-sm" data-property placeholder="Search by name or owner" aria-label="Property"></div>' +
|
|
'<button class="btn btn-sm btn-primary" data-save>Save</button>';
|
|
return '<div class="col-12 col-md-6 col-xl-3"><div class="border rounded p-3 h-100" data-slot="' + s.template_id + '">' +
|
|
'<div class="fw-semibold">' + esc(s.zone_name) + ' slot</div>' + controls +
|
|
'<div class="small mt-2">Showing: ' + showing + '</div>' + warn + (fullAuto ? '' : changed) +
|
|
'</div></div>';
|
|
}).join('');
|
|
if (fullAuto) return;
|
|
slots.forEach(function (s) {
|
|
var box = slotsEl.querySelector('[data-slot="' + s.template_id + '"]');
|
|
SearchSelect(box.querySelector('[data-property]'), {
|
|
value: s.picked ? s.property_id : '',
|
|
label: s.picked ? propLabel(s.picked) : '',
|
|
search: function (text) {
|
|
return api.get('/api/featured_properties/' + s.template_id + '/candidates?search=' + encodeURIComponent(text)).then(function (d) {
|
|
return (d.properties || []).map(function (p) { return { value: p.id, label: propLabel(p) }; });
|
|
});
|
|
}
|
|
});
|
|
});
|
|
};
|
|
var loadSlots = function () {
|
|
api.get('/api/featured_properties').then(function (d) {
|
|
if (!d.success) { slotsEl.innerHTML = '<div class="col text-danger">' + esc(d.error || 'Could not load the slots') + '</div>'; return; }
|
|
renderSlots(d);
|
|
}).catch(function () {});
|
|
};
|
|
modeInputs.forEach(function (el) {
|
|
el.addEventListener('change', function () {
|
|
if (!el.checked) return;
|
|
modeInputs.forEach(function (other) { other.disabled = true; });
|
|
api.action('/api/featured_properties', { full_auto: el.value === 'full_auto' }, el.value === 'full_auto' ? 'Top properties: full auto' : 'Top properties: per slot')
|
|
.then(renderSlots).catch(loadSlots);
|
|
});
|
|
});
|
|
slotsEl.addEventListener('change', function (e) {
|
|
if (!e.target.matches('[data-mode]')) return;
|
|
var box = e.target.closest('[data-slot]').querySelector('[data-property-box]');
|
|
box.hidden = e.target.value !== 'picked';
|
|
if (!box.hidden) box.querySelector('[data-property]').focus();
|
|
});
|
|
slotsEl.addEventListener('click', function (e) {
|
|
if (!e.target.matches('[data-save]')) return;
|
|
var box = e.target.closest('[data-slot]');
|
|
var mode = box.querySelector('[data-mode]').value;
|
|
var body = { mode: mode };
|
|
if (mode === 'picked') {
|
|
body.property_id = box.querySelector('[data-property]').dataset.value;
|
|
if (!body.property_id) { toast('Search for a property and pick one first', 'warning'); return; }
|
|
}
|
|
e.target.disabled = true;
|
|
api.action('/api/featured_properties/' + box.getAttribute('data-slot'), body, 'Top property slot saved').then(renderSlots).catch(function () { e.target.disabled = false; });
|
|
});
|
|
if (window.Live) { Live.on('featured_properties', loadSlots); Live.on('properties', loadSlots); }
|
|
loadSlots();
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|