437 lines
17 KiB
Plaintext
437 lines
17 KiB
Plaintext
<%- await include('parts/header.ejs', locals) %>
|
|
|
|
<style>
|
|
.collapsed {
|
|
height: 0;
|
|
}
|
|
.head-hover:hover {
|
|
cursor: pointer;
|
|
text-decoration: underline;
|
|
text-decoration-color: #d8dbe0;
|
|
}
|
|
.fix-pill-form {
|
|
min-width: 65px;
|
|
}
|
|
</style>
|
|
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8 mw-col6">
|
|
<div class="card card-accent-info">
|
|
<div class="card-body py-3">
|
|
<input
|
|
class="form-control my-1"
|
|
type="text"
|
|
name="resource"
|
|
id="resourceInput"
|
|
onkeyup="findResource()"
|
|
placeholder="Find resource by name..."
|
|
>
|
|
<div class="d-flex flex-wrap justify-content-xs-center justify-content-between pt-2" style="gap: 1rem;">
|
|
<div class="d-flex flex-grow-1x" style="gap: 1rem;">
|
|
<div class="d-flex flex-column flex-lg-row align-items-center" style="gap: 0.5rem;">
|
|
<span class="form-text text-muted text-center">
|
|
Default resources:
|
|
</span>
|
|
<label class="switch-lg c-switch c-switch-label c-switch-pill c-switch-success fix-pill-form">
|
|
<input type="checkbox" id="defResCheckbox" class="c-switch-input">
|
|
<span class="c-switch-slider" data-checked="Show" data-unchecked="Hide"></span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="d-flex flex-column flex-lg-row align-items-center" style="gap: 0.5rem;">
|
|
<span class="form-text text-muted text-center">
|
|
Only stopped resources:
|
|
</span>
|
|
<label class="switch-lg c-switch c-switch-label c-switch-pill c-switch-success fix-pill-form">
|
|
<input type="checkbox" id="stoppedResCheckbox" class="c-switch-input">
|
|
<span class="c-switch-slider" data-checked="Show" data-unchecked="Hide"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex" style="gap: 0.75rem;">
|
|
<button
|
|
class="btn btn-sm btn-outline-secondary float-right"
|
|
type="button"
|
|
id="btnExpandCollapse"
|
|
/>
|
|
<button class="btn btn-sm btn-primary float-right" type="button" id="btnRefresh" <%= disableActions %>>
|
|
<i class="icon-refresh"></i> Reload & Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row justify-content-center">
|
|
<div class="col-sm-12 col-lg-8 mw-col8">
|
|
<% for (const resGroup of resGroups) { %>
|
|
<div class="card table-responsive-sm" style="margin: 1em 0; border-radius: 0;" id="resList-card-<%= resGroup.divName %>">
|
|
<!-- Table class is responsible for the sharper edges -->
|
|
<table class="table table-hover table-outline mb-0">
|
|
<thead class="thead-light head-hover">
|
|
<tr>
|
|
<th class="text-break text-center" colspan="2">
|
|
<%= resGroup.subPath %>
|
|
<span class="toggle-icon" id="icon-<%= resGroup.divName %>">
|
|
<i class="icon-arrow-down" style="font-size: 75%; font-weight: bold;"></i>
|
|
</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="resList-cardBody-<%= resGroup.divName %>" aria-labelledby="resList-cardHeader-<%= resGroup.divName %>" class="mb-1">
|
|
<% for (const resource of resGroup.resources) { %>
|
|
<tr id="res-<%= resource.divName %>">
|
|
<td>
|
|
<strong style="word-wrap: break-word;"><%= resource.name %></strong>
|
|
<% if (resource.version !== '') { %>
|
|
<em><%= resource.version %></em>
|
|
<% } %>
|
|
<% if (resource.author !== '') { %>
|
|
by <%= resource.author %>
|
|
<% } %>
|
|
<% if (resource.description !== '') { %>
|
|
<br/>
|
|
<%= resource.description %>
|
|
<% } %>
|
|
</td>
|
|
<td class="tableActions">
|
|
<% if (resource.status === 'started') { %>
|
|
<a class="btn btn-sm btn-outline-warning d-block d-md-inline-block mr-md-1 mb-1 mb-md-0 <%= disableActions %>"
|
|
href="#"
|
|
onclick="btnCommand('ensure_res', '<%= resource.name %>'); event.preventDefault();" <%= disableActions %>>
|
|
Restart
|
|
</a>
|
|
<a class="btn btn-sm btn-outline-danger d-block d-md-inline-block m-0 <%= disableActions %>"
|
|
href="#"
|
|
onclick="btnCommand('stop_res', '<%= resource.name %>'); event.preventDefault();" <%= disableActions %>>
|
|
Stop
|
|
</a>
|
|
<% } else { %>
|
|
<a class="btn btn-sm btn-outline-success d-block d-md-inline-block <%= disableActions %>"
|
|
href="#"
|
|
onclick="btnCommand('ensure_res', '<%= resource.name %>'); event.preventDefault();" <%= disableActions %>>
|
|
Start
|
|
</a>
|
|
<% } %>
|
|
</td>
|
|
</tr>
|
|
<% } %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<%- await include('parts/footer.ejs', locals) %>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
//NOTE: this is hacky af, but whatever
|
|
const resGroupsJS = JSON.parse(atob(`<%= (Buffer.from(resGroupsJS).toString('base64')) %>`));
|
|
|
|
const defaultResources = [
|
|
"baseevents",
|
|
"basic-gamemode",
|
|
"betaguns", //old
|
|
"channelfeed", //old
|
|
"chat-theme-gtao",
|
|
"chat",
|
|
"example-loadscreen",
|
|
"fivem-awesome1501", //old
|
|
"fivem-map-hipster",
|
|
"fivem-map-skater",
|
|
"fivem",
|
|
"gameInit", //old
|
|
"hardcap",
|
|
"irc", //old
|
|
"keks", //old
|
|
"mapmanager",
|
|
"money-fountain-example-map",
|
|
"money-fountain",
|
|
"money",
|
|
"monitor",
|
|
"obituary-deaths", //old
|
|
"obituary", //old
|
|
"ped-money-drops",
|
|
"player-data",
|
|
"playernames",
|
|
"race-test", //old
|
|
"race", //old
|
|
"rconlog",
|
|
"redm-map-one",
|
|
"runcode",
|
|
"scoreboard",
|
|
"sessionmanager-rdr3",
|
|
"sessionmanager",
|
|
"spawnmanager",
|
|
"webadmin",
|
|
"webpack",
|
|
"yarn",
|
|
]
|
|
|
|
const convErrorType = (x) => x === 'error' ? 'danger' : x;
|
|
|
|
//============================================== Page State
|
|
let collapsedGroups = new Set();
|
|
try {
|
|
const storedCollapsedGroups = window.localStorage.resourcesPageCollapsedGroups;
|
|
if (storedCollapsedGroups) {
|
|
const parsedCollapsedGroups = JSON.parse(storedCollapsedGroups);
|
|
if (Array.isArray(parsedCollapsedGroups)) {
|
|
collapsedGroups = new Set(parsedCollapsedGroups);
|
|
} else {
|
|
throw new Error('Invalid storedCollapsedGroups');
|
|
}
|
|
} else {
|
|
window.localStorage.resourcesPageCollapsedGroups = '[]';
|
|
}
|
|
} catch (error) {
|
|
console.warn('Error parsing storedCollapsedGroups:', error);
|
|
window.localStorage.resourcesPageCollapsedGroups = '[]';
|
|
}
|
|
const saveCollapsedGroups = () => {
|
|
window.localStorage.resourcesPageCollapsedGroups = JSON.stringify([...collapsedGroups.values()]);
|
|
}
|
|
|
|
|
|
$('#defResCheckbox').click(() => {
|
|
window.localStorage.resourcesPageShowDefault = document.getElementById('defResCheckbox').checked;
|
|
refreshResourceList();
|
|
});
|
|
|
|
$('#stoppedResCheckbox').click(() => {
|
|
window.localStorage.resourcesPageShowStopped = document.getElementById('stoppedResCheckbox').checked;
|
|
refreshResourceList();
|
|
});
|
|
|
|
|
|
//============================================== Refresh List
|
|
$('#btnRefresh').click(() => {
|
|
const notify = $.notify({ message: '<p class="text-center">Executing Command...</p>' }, {});
|
|
txAdminAPI({
|
|
type: "POST",
|
|
url: '/fxserver/commands',
|
|
timeout: REQ_TIMEOUT_LONG,
|
|
data: { action: 'refresh_res', parameter: '' },
|
|
success: function (data) {
|
|
if(data.type !== 'error'){
|
|
window.location.reload(true);
|
|
}else{
|
|
notify.update('progress', 0);
|
|
notify.update('type', convErrorType(data.type));
|
|
notify.update('message', data.msg);
|
|
}
|
|
},
|
|
error: function (xmlhttprequest, textstatus, message) {
|
|
notify.update('progress', 0);
|
|
notify.update('type', 'danger');
|
|
notify.update('message', message);
|
|
}
|
|
});
|
|
})
|
|
|
|
|
|
//============================================== Start/Stop/Restart buttons
|
|
function btnCommand(action, parameter) {
|
|
const notify = $.notify({ message: '<p class="text-center">Executing Command...</p>' }, {});
|
|
txAdminAPI({
|
|
type: "POST",
|
|
url: '/fxserver/commands',
|
|
data: { action, parameter },
|
|
success: function (data) {
|
|
if(data.type !== 'error'){
|
|
window.location.reload(true);
|
|
}else{
|
|
notify.update('progress', 0);
|
|
notify.update('type', convErrorType(data.type));
|
|
notify.update('message', data.msg);
|
|
}
|
|
},
|
|
error: function (xmlhttprequest, textstatus, message) {
|
|
notify.update('progress', 0);
|
|
notify.update('type', 'danger');
|
|
notify.update('message', message);
|
|
}
|
|
});
|
|
}
|
|
|
|
//============================================== Search function
|
|
function findResource() {
|
|
const inputEl = document.getElementById("resourceInput");
|
|
localStorage.setItem("resourcesPageFilter", inputEl.value ?? '');
|
|
const filter = inputEl.value.toUpperCase();
|
|
|
|
resGroupsJS.forEach(folder => {
|
|
let hidden = 0;
|
|
folder.resources.forEach(resource => {
|
|
if (resource.name.toUpperCase().indexOf(filter) > -1) {
|
|
$(`#res-${resource.divName}`).show();
|
|
} else {
|
|
hidden++;
|
|
$(`#res-${resource.divName}`).hide();
|
|
}
|
|
});
|
|
if (folder.resources.length == hidden) {
|
|
$(`#resList-card-${folder.divName}`).hide();
|
|
} else {
|
|
$(`#resList-card-${folder.divName}`).show();
|
|
}
|
|
});
|
|
if(!filter.length){
|
|
refreshResourceList();
|
|
}
|
|
}
|
|
|
|
//============================================== Expand/Collapse All
|
|
let isExpandButton = false;
|
|
const btnExpandCollapse = document.getElementById('btnExpandCollapse');
|
|
const btnExpandCollapseIcon = btnExpandCollapse.querySelector('i');
|
|
function updateExpandCollapseBtn() {
|
|
let expandedCount = 0;
|
|
let collapsedCount = 0;
|
|
resGroupsJS.forEach(folder => {
|
|
const card = document.getElementById(`resList-card-${folder.divName}`);
|
|
if (card.querySelector('tbody').classList.contains('collapse')) {
|
|
collapsedCount++;
|
|
} else {
|
|
expandedCount++;
|
|
}
|
|
});
|
|
if (expandedCount === resGroupsJS.length) {
|
|
isExpandButton = false;
|
|
btnExpandCollapse.innerHTML = '<i class="icon-size-actual"></i> Collapse All';
|
|
} else {
|
|
isExpandButton = true;
|
|
btnExpandCollapse.innerHTML = '<i class="icon-size-fullscreen"></i> Expand All';
|
|
}
|
|
}
|
|
$('#btnExpandCollapse').click(() => {
|
|
if (isExpandButton) {
|
|
resGroupsJS.forEach(folder => {
|
|
const card = document.getElementById(`resList-card-${folder.divName}`);
|
|
toggleResGroup(card, false, true);
|
|
});
|
|
} else {
|
|
resGroupsJS.forEach(folder => {
|
|
const card = document.getElementById(`resList-card-${folder.divName}`);
|
|
toggleResGroup(card, false, false);
|
|
});
|
|
}
|
|
updateExpandCollapseBtn();
|
|
})
|
|
|
|
//============================================== Utils
|
|
function refreshResourceList() {
|
|
if ($('#defResCheckbox').is(':checked')) {
|
|
defaultResources.forEach(defRes => {
|
|
$(`#res-${defRes}`).show();
|
|
$(`#res-${defRes}`).removeClass("defaultRes");
|
|
|
|
$(`#res-${defRes}`).closest('div').show();
|
|
})
|
|
} else {
|
|
defaultResources.forEach(defRes => {
|
|
const trElement = $(`#res-${defRes}`);
|
|
|
|
trElement.hide();
|
|
trElement.addClass("defaultRes");
|
|
|
|
$(`#res-${defRes}`).closest('div').hide();
|
|
})
|
|
}
|
|
|
|
const onlyStopped = $('#stoppedResCheckbox').is(':checked')
|
|
resGroupsJS.forEach(folder => {
|
|
let hidden = 0;
|
|
|
|
folder.resources.forEach(resource => {
|
|
if ($(`#res-${resource.divName}`).hasClass("defaultRes")) {
|
|
hidden++;
|
|
return
|
|
}
|
|
|
|
if (onlyStopped && resource.status !== 'stopped') {
|
|
$(`#res-${resource.divName}`).hide()
|
|
hidden++;
|
|
} else {
|
|
$(`#res-${resource.divName}`).show()
|
|
}
|
|
});
|
|
|
|
const card = $(`#resList-card-${folder.divName}`);
|
|
if (folder.resources.length === hidden) {
|
|
card.hide();
|
|
} else {
|
|
card.show();
|
|
}
|
|
if (collapsedGroups.has(folder.divName)) {
|
|
toggleResGroup(card[0], false, false);
|
|
} else {
|
|
toggleResGroup(card[0], false, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function toggleResGroup(groupCardElement, single, show) {
|
|
const tbody = groupCardElement.querySelector("tbody");
|
|
const icon = groupCardElement.querySelector('.toggle-icon i');
|
|
if (show === undefined) {
|
|
show = tbody.classList.contains('collapse');
|
|
}
|
|
const groupDivName = groupCardElement.id.split('-').pop();
|
|
if (show) {
|
|
tbody.classList.remove('collapse');
|
|
icon.classList.remove('icon-arrow-down');
|
|
icon.classList.add('icon-arrow-up');
|
|
if (single) {
|
|
groupCardElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
collapsedGroups.delete(groupDivName);
|
|
} else {
|
|
tbody.classList.add('collapse');
|
|
icon.classList.add('icon-arrow-down');
|
|
icon.classList.remove('icon-arrow-up');
|
|
collapsedGroups.add(groupDivName);
|
|
}
|
|
saveCollapsedGroups();
|
|
}
|
|
|
|
$(() => {
|
|
if (typeof window.localStorage.resourcesPageShowDefault === 'string') {
|
|
document.getElementById('defResCheckbox').checked = (window.localStorage.resourcesPageShowDefault === 'true');
|
|
} else {
|
|
window.localStorage.resourcesPageShowDefault = false;
|
|
}
|
|
let hasFilter = false;
|
|
if (typeof window.localStorage.resourcesPageFilter === 'string' && window.localStorage.resourcesPageFilter.length) {
|
|
document.getElementById("resourceInput").value = window.localStorage.resourcesPageFilter;
|
|
hasFilter = true;
|
|
}
|
|
|
|
document.getElementById('stoppedResCheckbox').checked = window.localStorage.resourcesPageShowStopped === 'true';
|
|
|
|
//Make the theads clickable
|
|
resGroupsJS.forEach(folder => {
|
|
const card = document.getElementById(`resList-card-${folder.divName}`);
|
|
const thead = card.querySelector('thead');
|
|
thead.addEventListener('click', () => {
|
|
toggleResGroup(card, true);
|
|
updateExpandCollapseBtn();
|
|
});
|
|
});
|
|
|
|
refreshResourceList();
|
|
updateExpandCollapseBtn();
|
|
if(hasFilter){
|
|
findResource();
|
|
}
|
|
});
|
|
</script>
|