728 lines
29 KiB
HTML
728 lines
29 KiB
HTML
@(deciders: Seq[repository.Decider])(implicit request: Request[_], messages: Messages)
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="csrf-token" content="@{play.filters.csrf.CSRF.getToken.map(_.value).getOrElse("")}">
|
||
<title>Admin — Decider</title>
|
||
<link rel='stylesheet' href='@routes.Assets.at("lib/bootstrap/css/bootstrap.min.css")'>
|
||
<style>
|
||
body { padding-top: 70px; }
|
||
.decider-card { margin-bottom: 15px; border-left: 4px solid #337ab7; }
|
||
/* Строка карты занимает всю доступную ширину: выбор карты фиксированный, описание растягивается */
|
||
.map-row { margin-bottom: 5px; display: flex; align-items: center; }
|
||
.map-row .map-desc { flex: 1 1 auto; min-width: 0; width: auto; }
|
||
.map-row .map-preview, .map-row .btn-remove-map { flex: 0 0 auto; }
|
||
.modal-footer { text-align: right; }
|
||
.btn-remove-map { margin-left: 5px; }
|
||
.decider-key { font-family: monospace; color: #666; font-size: 0.9em; }
|
||
.nav-tabs > li > a { cursor: pointer; }
|
||
#deciderList { max-height: calc(100vh - 200px); overflow-y: auto; }
|
||
#deciderList .list-group-item { cursor: pointer; }
|
||
#deciderList .list-group-item.active { background-color: #337ab7; border-color: #337ab7; }
|
||
#deciderList .list-group-item:hover { background-color: #f5f5f5; }
|
||
#deciderList .list-group-item.active:hover { background-color: #286090; }
|
||
.error-msg { color: red; margin-top: 10px; }
|
||
.success-msg { color: green; margin-top: 10px; }
|
||
.map-preview { width: 64px; height: 40px; object-fit: cover; border-radius: 3px; margin-right: 6px; vertical-align: middle; }
|
||
/* Кнопка выбора карты вместо ручного ввода имени */
|
||
.btn-select-map { width: 170px; flex: 0 0 170px; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 6px; }
|
||
.map-gallery { max-height: 60vh; overflow-y: auto; }
|
||
/* Поиск карт и загрузка картинки */
|
||
.map-picker-toolbar { display: flex; align-items: center; margin-bottom: 10px; }
|
||
.map-picker-toolbar .map-search { flex: 1 1 auto; margin-right: 8px; }
|
||
#mapUploadMessage { min-height: 18px; margin-bottom: 8px; }
|
||
.map-gallery .map-thumb { cursor: pointer; text-align: center; margin-bottom: 10px; padding: 4px; border: 1px solid transparent; border-radius: 4px; }
|
||
.map-gallery .map-thumb:hover { border-color: #337ab7; background-color: #f5f5f5; }
|
||
.map-gallery .map-thumb img { width: 100%; max-width: 160px; height: 100px; object-fit: cover; border-radius: 4px; }
|
||
.map-gallery .map-thumb-name { font-size: 11px; word-break: break-all; margin-top: 3px; }
|
||
#mapPickerTabs { margin-bottom: 15px; }
|
||
|
||
/* Шапка: название слева, Steam ID и Refresh справа, всё по центру по вертикали */
|
||
.navbar .container { display: flex; align-items: center; justify-content: space-between; }
|
||
.navbar-header { float: none; }
|
||
.navbar-user { display: flex; align-items: center; margin-left: 15px; color: #ccc; }
|
||
.navbar-user .navbar-user-id { margin-right: 10px; white-space: nowrap; }
|
||
.navbar-user .btn { margin: 0; }
|
||
|
||
/* Кнопки в заголовках панелей по центру строки с заголовком */
|
||
.panel-heading .panel-title { display: flex; align-items: center; justify-content: space-between; }
|
||
.panel-heading .panel-title > .btn { flex: 0 0 auto; margin-left: 10px; }
|
||
|
||
@@media (max-width: 480px) {
|
||
.navbar-user .navbar-user-id { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||
<div class="container">
|
||
<div class="navbar-header">
|
||
<a class="navbar-brand" href="/decider/admin">Decider admin</a>
|
||
</div>
|
||
<div class="navbar-user">
|
||
<span class="navbar-user-id">Steam ID: @(request.session.get("steamId").getOrElse("unknown"))</span>
|
||
<a href="/decider/admin" class="btn btn-sm btn-default">Refresh</a>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="container">
|
||
<div class="row">
|
||
<!-- Left panel: list of deciders -->
|
||
<div class="col-md-4">
|
||
<div class="panel panel-default">
|
||
<div class="panel-heading">
|
||
<h3 class="panel-title">
|
||
Deciders (@deciders.size)
|
||
<button class="btn btn-sm btn-success" onclick="openNewDecider()">+ New</button>
|
||
</h3>
|
||
</div>
|
||
<div class="list-group" id="deciderList">
|
||
@for(d <- deciders) {
|
||
<a class="list-group-item" data-key="@d.key" onclick="openDecider('@d.key')">
|
||
<h4 class="list-group-item-heading">@d.name</h4>
|
||
<p class="list-group-item-text decider-key">@d.key • maps: @d.maps.size • owner: @d.ownerSteamId.getOrElse("-")</p>
|
||
</a>
|
||
}
|
||
@if(deciders.isEmpty) {
|
||
<div class="list-group-item text-muted">No deciders</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right panel: edit form -->
|
||
<div class="col-md-8">
|
||
<div class="panel panel-default" id="editPanel" style="display: none;">
|
||
<div class="panel-heading">
|
||
<h3 class="panel-title">
|
||
<span id="editTitle">Edit</span>
|
||
<button class="btn btn-sm btn-danger" id="btnDelete" onclick="deleteCurrentDecider()">Delete</button>
|
||
</h3>
|
||
</div>
|
||
<div class="panel-body">
|
||
<form id="deciderForm">
|
||
<input type="hidden" id="deciderKey">
|
||
|
||
<div class="form-group">
|
||
<label for="formKey">Key</label>
|
||
<input type="text" class="form-control" id="formKey" placeholder="e.g. dawn_of_war">
|
||
<p class="help-block">Unique decider identifier (letters, digits, hyphen, underscore)</p>
|
||
<p class="help-block" id="keyUrlBlock" style="display: none;">
|
||
URL: <a id="formKeyUrl" href="#" target="_blank" rel="noopener"></a>
|
||
<button type="button" class="btn btn-xs btn-default" id="btnCopyUrl"
|
||
title="Copy URL to clipboard">Copy</button>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="formOwner">Owner Steam ID</label>
|
||
<input type="text" class="form-control" id="formOwner" readonly>
|
||
<p class="help-block">Owner cannot be changed</p>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="formName">Name</label>
|
||
<input type="text" class="form-control" id="formName" required>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="formRaceCount">Race Count</label>
|
||
<input type="number" class="form-control" id="formRaceCount" min="0">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="formRules">Rules</label>
|
||
<textarea class="form-control" id="formRules" rows="5"></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="checkbox">
|
||
<label>
|
||
<input type="checkbox" id="formExistAdditionalRaces"> Additional Races (Race count per team doubled)
|
||
</label>
|
||
</div>
|
||
<div class="checkbox">
|
||
<label>
|
||
<input type="checkbox" id="formExistTextFieldRaces"> Exist Text Field Races (for Unification like mods)
|
||
</label>
|
||
</div>
|
||
<div class="checkbox">
|
||
<label>
|
||
<input type="checkbox" id="formDescription">Map description
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<h4>Maps</h4>
|
||
<div id="mapsContainer"></div>
|
||
<button type="button" class="btn btn-sm btn-primary" onclick="openMapPicker()">+ Add map</button>
|
||
|
||
<hr>
|
||
<div class="form-group text-right">
|
||
<button type="button" class="btn btn-default" onclick="closeEditPanel()">Cancel</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveDecider()">Save</button>
|
||
</div>
|
||
<div id="saveMessage"></div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="emptyState" class="text-muted text-center" style="padding: 50px;">
|
||
<h3>Select a decider to edit</h3>
|
||
<p>or create a new one</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Delete confirmation modal -->
|
||
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||
<h4 class="modal-title">Confirm deletion</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>Are you sure you want to delete decider <strong id="deleteDeciderName"></strong>?</p>
|
||
<p class="text-danger">This action cannot be undone!</p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Delete</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Map image picker modal -->
|
||
<div class="modal fade" id="mapPickerModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||
<h4 class="modal-title">Choose a map</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="map-picker-toolbar">
|
||
<input type="search" id="mapSearch" class="form-control map-search" placeholder="Search maps..." autocomplete="off">
|
||
<button type="button" class="btn btn-sm btn-primary" id="btnUploadMap">Upload image</button>
|
||
<input type="file" id="mapUploadInput" accept="image/jpeg,image/png,image/webp" style="display: none;">
|
||
</div>
|
||
<div id="mapUploadMessage"></div>
|
||
<ul class="nav nav-pills" id="mapPickerTabs"></ul>
|
||
<div class="row map-gallery" id="mapGallery"></div>
|
||
<div id="mapPickerEmpty" class="text-muted" style="display: none;">No map images found</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src='@routes.Assets.at("lib/jquery/jquery.min.js")'></script>
|
||
<script src='@routes.Assets.at("lib/bootstrap/js/bootstrap.min.js")'></script>
|
||
<script src='@routes.Assets.at("lib/underscore/underscore.js")'></script>
|
||
<script>
|
||
var currentDeciderKey = null;
|
||
// Steam ID текущего пользователя: владелец нового десайдера.
|
||
var currentSteamId = '@request.session.get("steamId").getOrElse("")';
|
||
|
||
// CSRF: все AJAX-запросы отправляем с токеном из meta-тега,
|
||
// иначе Play отклоняет POST/DELETE с JSON-телом.
|
||
(function() {
|
||
var token = $('meta[name="csrf-token"]').attr('content');
|
||
if (token) {
|
||
$.ajaxSetup({ headers: { 'Csrf-Token': token } });
|
||
}
|
||
})();
|
||
|
||
// Open decider for editing
|
||
function openDecider(key) {
|
||
currentDeciderKey = key;
|
||
$.ajax({
|
||
url: '/decider/admin/deciders/' + encodeURIComponent(key),
|
||
method: 'GET',
|
||
success: function(decider) {
|
||
showEditPanel(decider);
|
||
},
|
||
error: function() {
|
||
alert('Failed to load decider');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Open new decider form
|
||
function openNewDecider() {
|
||
currentDeciderKey = null;
|
||
showEditPanel({
|
||
key: '',
|
||
name: '',
|
||
rules: '',
|
||
raceCount: 1,
|
||
existAdditionalRaces: false,
|
||
existTextFieldRaces: false,
|
||
description: false,
|
||
maps: []
|
||
});
|
||
}
|
||
|
||
// Show edit panel with decider data
|
||
function showEditPanel(decider) {
|
||
$('#emptyState').hide();
|
||
$('#editPanel').show();
|
||
$('#editTitle').text(decider.key ? 'Edit: ' + decider.name : 'New decider');
|
||
|
||
$('#deciderKey').val(decider.key);
|
||
$('#formKey').val(decider.key).prop('readonly', !!decider.key);
|
||
|
||
// Ссылка на десайдер: показываем только для уже созданных.
|
||
if (decider.key) {
|
||
var deciderPath = '/decider/' + encodeURIComponent(decider.key);
|
||
$('#formKeyUrl')
|
||
.attr('href', deciderPath)
|
||
.text(window.location.origin + deciderPath);
|
||
$('#keyUrlBlock').show();
|
||
} else {
|
||
$('#formKeyUrl').attr('href', '#').text('');
|
||
$('#keyUrlBlock').hide();
|
||
}
|
||
|
||
$('#btnDelete').toggle(!!decider.key);
|
||
$('#formOwner').val(decider.ownerSteamId || currentSteamId);
|
||
$('#formName').val(decider.name);
|
||
$('#formRules').val(decider.rules);
|
||
$('#formRaceCount').val(decider.raceCount);
|
||
$('#formExistAdditionalRaces').prop('checked', decider.existAdditionalRaces);
|
||
$('#formExistTextFieldRaces').prop('checked', decider.existTextFieldRaces);
|
||
$('#formDescription').prop('checked', decider.description);
|
||
|
||
// Render maps
|
||
var container = $('#mapsContainer');
|
||
container.empty();
|
||
if (decider.maps && decider.maps.length > 0) {
|
||
decider.maps.forEach(function(map, i) {
|
||
addMapRow(map.map, map.description);
|
||
});
|
||
} else {
|
||
addMapRow();
|
||
}
|
||
|
||
// Превью карт: подгружаем список картинок, чтобы найти файлы любого формата.
|
||
ensureMapImages();
|
||
|
||
// Highlight selected in list (точное совпадение по ключу десайдера)
|
||
$('.list-group-item').removeClass('active');
|
||
if (decider.key) {
|
||
$('.list-group-item').filter(function() {
|
||
return $(this).attr('data-key') === decider.key;
|
||
}).addClass('active');
|
||
}
|
||
}
|
||
|
||
// Close edit panel
|
||
function closeEditPanel() {
|
||
$('#editPanel').hide();
|
||
$('#emptyState').show();
|
||
currentDeciderKey = null;
|
||
}
|
||
|
||
// ---------- Map image picker ----------
|
||
|
||
var mapImages = null;
|
||
var pickerTargetRow = null;
|
||
var activeMapGroup = 2;
|
||
var mapSearchQuery = '';
|
||
|
||
// Группы карт: 1x1, 2x2, 3x3, 4x4 и остальные.
|
||
var mapGroups = [
|
||
{ players: 2, title: '1x1 (2p)' },
|
||
{ players: 4, title: '2x2 (4p)' },
|
||
{ players: 6, title: '3x3 (6p)' },
|
||
{ players: 8, title: '4x4 (8p)' },
|
||
{ players: 0, title: 'Other' }
|
||
];
|
||
|
||
function openMapPicker(targetRow) {
|
||
pickerTargetRow = targetRow || null;
|
||
mapSearchQuery = '';
|
||
$('#mapSearch').val('');
|
||
$('#mapUploadMessage').text('');
|
||
if (mapImages) {
|
||
showMapPicker();
|
||
return;
|
||
}
|
||
$.ajax({
|
||
url: '/decider/admin/maps',
|
||
method: 'GET',
|
||
success: function(data) {
|
||
mapImages = data;
|
||
showMapPicker();
|
||
},
|
||
error: function() {
|
||
alert('Failed to load map images');
|
||
}
|
||
});
|
||
}
|
||
|
||
function showMapPicker() {
|
||
// Если в текущей группе нет карт, открываем первую непустую.
|
||
if (mapsInGroup(activeMapGroup).length === 0) {
|
||
var firstNonEmpty = _.find(mapGroups, function(g) {
|
||
return mapsInGroup(g.players).length > 0;
|
||
});
|
||
activeMapGroup = firstNonEmpty ? firstNonEmpty.players : 2;
|
||
}
|
||
renderMapTabs();
|
||
renderMapGallery();
|
||
$('#mapPickerModal').modal('show');
|
||
}
|
||
|
||
function mapsInGroup(players) {
|
||
return (mapImages || []).filter(function(m) {
|
||
return players === 0
|
||
? [2, 4, 6, 8].indexOf(m.players) === -1
|
||
: m.players === players;
|
||
});
|
||
}
|
||
|
||
// Карты для галереи: при активном поиске ищем по всем группам сразу.
|
||
function visibleMaps() {
|
||
if (!mapSearchQuery) {
|
||
return mapsInGroup(activeMapGroup);
|
||
}
|
||
return (mapImages || []).filter(function(m) {
|
||
return (m.name || '').toLowerCase().indexOf(mapSearchQuery) !== -1 ||
|
||
(m.file || '').toLowerCase().indexOf(mapSearchQuery) !== -1;
|
||
});
|
||
}
|
||
|
||
function renderMapTabs() {
|
||
var tabs = $('#mapPickerTabs').empty();
|
||
// Во время поиска вкладки групп не нужны: показываются все совпадения.
|
||
if (mapSearchQuery) {
|
||
tabs.hide();
|
||
return;
|
||
}
|
||
tabs.show();
|
||
mapGroups.forEach(function(group) {
|
||
var count = mapsInGroup(group.players).length;
|
||
if (group.players === 0 && count === 0) return;
|
||
var link = $('<a href="#">')
|
||
.text(group.title + ' (' + count + ')')
|
||
.on('click', function(e) {
|
||
e.preventDefault();
|
||
activeMapGroup = group.players;
|
||
renderMapTabs();
|
||
renderMapGallery();
|
||
});
|
||
tabs.append($('<li>').toggleClass('active', group.players === activeMapGroup).append(link));
|
||
});
|
||
}
|
||
|
||
function renderMapGallery() {
|
||
var gallery = $('#mapGallery').empty();
|
||
var maps = visibleMaps();
|
||
maps.forEach(function(map) {
|
||
var thumb = $('<div class="map-thumb">')
|
||
.append($('<img>').attr('src', mapImageUrl(map.file)).attr('alt', map.name).attr('title', map.name))
|
||
.append($('<div class="map-thumb-name">').text(map.name))
|
||
.on('click', function() {
|
||
chooseMap(map.name);
|
||
});
|
||
gallery.append($('<div class="col-xs-6 col-sm-4 col-md-3">').append(thumb));
|
||
});
|
||
$('#mapPickerEmpty')
|
||
.text(mapSearchQuery ? 'No maps match your search' : 'No map images found')
|
||
.toggle(maps.length === 0);
|
||
}
|
||
|
||
// Перечитывает список картинок с сервера и перерисовывает галерею.
|
||
function reloadMapImages(preferredGroup) {
|
||
$.ajax({
|
||
url: '/decider/admin/maps',
|
||
method: 'GET',
|
||
success: function(data) {
|
||
mapImages = data;
|
||
if (preferredGroup !== undefined) {
|
||
activeMapGroup = preferredGroup;
|
||
}
|
||
renderMapTabs();
|
||
renderMapGallery();
|
||
},
|
||
error: function() {
|
||
alert('Failed to load map images');
|
||
}
|
||
});
|
||
}
|
||
|
||
// ---------- Поиск и загрузка картинок карт ----------
|
||
|
||
$('#mapSearch').on('input', function() {
|
||
mapSearchQuery = $(this).val().trim().toLowerCase();
|
||
renderMapTabs();
|
||
renderMapGallery();
|
||
});
|
||
|
||
$('#btnUploadMap').on('click', function() {
|
||
$('#mapUploadInput').click();
|
||
});
|
||
|
||
$('#mapUploadInput').on('change', function() {
|
||
var input = this;
|
||
var file = input.files && input.files[0];
|
||
if (!file) return;
|
||
|
||
var formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
var msgEl = $('#mapUploadMessage');
|
||
msgEl.removeClass('text-danger').addClass('text-muted').text('Uploading...');
|
||
|
||
$.ajax({
|
||
url: '/decider/admin/maps',
|
||
method: 'POST',
|
||
data: formData,
|
||
processData: false,
|
||
contentType: false,
|
||
success: function(image) {
|
||
msgEl.removeClass('text-danger').text('');
|
||
input.value = '';
|
||
mapSearchQuery = '';
|
||
$('#mapSearch').val('');
|
||
reloadMapImages(image ? image.players : undefined);
|
||
},
|
||
error: function(xhr) {
|
||
var error = 'Upload failed';
|
||
if (xhr.responseJSON && xhr.responseJSON.error) {
|
||
error = xhr.responseJSON.error;
|
||
}
|
||
msgEl.removeClass('text-muted').addClass('text-danger').text(error);
|
||
input.value = '';
|
||
}
|
||
});
|
||
});
|
||
|
||
function chooseMap(mapName) {
|
||
if (pickerTargetRow && $.contains(document, pickerTargetRow[0])) {
|
||
pickerTargetRow.find('.map-name').val(mapName);
|
||
updateMapRow(pickerTargetRow);
|
||
} else {
|
||
addMapRow(mapName, '');
|
||
}
|
||
pickerTargetRow = null;
|
||
$('#mapPickerModal').modal('hide');
|
||
}
|
||
|
||
function mapImageUrl(file) {
|
||
return '/assets/images/maps/' + encodeURIComponent(file);
|
||
}
|
||
|
||
// ---------- Map rows ----------
|
||
|
||
// Add map row
|
||
function addMapRow(mapName, description) {
|
||
var row = $('<div class="map-row form-inline">');
|
||
|
||
var preview = $('<img class="map-preview" alt="">');
|
||
// Имя карты задаётся только через выбор из галереи, ручной ввод запрещён.
|
||
var nameInput = $('<input type="hidden" class="map-name">')
|
||
.val(mapName || '');
|
||
var descInput = $('<input type="text" class="form-control map-desc" placeholder="Description">')
|
||
.val(description || '');
|
||
var selectBtn = $('<button type="button" class="btn btn-sm btn-default btn-select-map">Select map</button>')
|
||
.on('click', function() {
|
||
openMapPicker(row);
|
||
});
|
||
var removeBtn = $('<button type="button" class="btn btn-sm btn-danger btn-remove-map" title="Remove map">✕</button>')
|
||
.on('click', function() {
|
||
row.remove();
|
||
});
|
||
|
||
row.append(preview, nameInput, selectBtn, descInput, removeBtn);
|
||
$('#mapsContainer').append(row);
|
||
updateMapRow(row);
|
||
}
|
||
|
||
// Обновляет превью и подпись кнопки выбора карты
|
||
function updateMapRow(row) {
|
||
var mapName = row.find('.map-name').val().trim();
|
||
var selectBtn = row.find('.btn-select-map');
|
||
selectBtn.text(mapName || 'Select map').attr('title', mapName || 'Select map');
|
||
|
||
var img = row.find('.map-preview').off('error').off('load');
|
||
if (!mapName) {
|
||
img.hide().removeAttr('src');
|
||
return;
|
||
}
|
||
img.attr('src', mapImageUrl(mapImageFileFor(mapName))).show().on('error', function() {
|
||
$(this).hide();
|
||
});
|
||
}
|
||
|
||
// Имя файла картинки для превью: если список карт загружен, берём реальный
|
||
// файл (он может быть png/webp), иначе считаем, что это jpg.
|
||
function mapImageFileFor(mapName) {
|
||
var found = _.find(mapImages || [], function(m) {
|
||
return m.name === mapName;
|
||
});
|
||
if (found) return found.file;
|
||
return /\.(jpg|jpeg|png|webp)$/i.test(mapName) ? mapName : mapName + '.jpg';
|
||
}
|
||
|
||
// Догружает список картинок, чтобы превью находили файлы любого формата.
|
||
function ensureMapImages() {
|
||
if (mapImages) return;
|
||
$.ajax({
|
||
url: '/decider/admin/maps',
|
||
method: 'GET',
|
||
success: function(data) {
|
||
mapImages = data;
|
||
$('#mapsContainer .map-row').each(function() {
|
||
updateMapRow($(this));
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Remove map row
|
||
function removeMapRow(btn) {
|
||
$(btn).closest('.map-row').remove();
|
||
}
|
||
|
||
// Collect form data and save
|
||
function saveDecider() {
|
||
var key = $('#formKey').val().trim();
|
||
if (!key) {
|
||
alert('Key is required');
|
||
return;
|
||
}
|
||
if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
|
||
alert('Key may contain only letters, digits, hyphen and underscore');
|
||
return;
|
||
}
|
||
|
||
var maps = [];
|
||
$('#mapsContainer .map-row').each(function() {
|
||
var mapName = $(this).find('.map-name').val().trim();
|
||
var desc = $(this).find('.map-desc').val().trim();
|
||
if (mapName) {
|
||
maps.push({ map: mapName, description: desc || null });
|
||
}
|
||
});
|
||
|
||
var data = {
|
||
key: key,
|
||
name: $('#formName').val().trim(),
|
||
rules: $('#formRules').val(),
|
||
raceCount: parseInt($('#formRaceCount').val()) || 1,
|
||
existAdditionalRaces: $('#formExistAdditionalRaces').is(':checked'),
|
||
existTextFieldRaces: $('#formExistTextFieldRaces').is(':checked'),
|
||
description: $('#formDescription').is(':checked'),
|
||
maps: maps
|
||
};
|
||
|
||
var msgEl = $('#saveMessage');
|
||
msgEl.html('<span class="text-muted">Saving...</span>');
|
||
|
||
var url = '/decider/admin/deciders';
|
||
var method = 'POST';
|
||
|
||
$.ajax({
|
||
url: url,
|
||
method: method,
|
||
contentType: 'application/json',
|
||
data: JSON.stringify(data),
|
||
success: function() {
|
||
msgEl.html('<span class="success-msg">Saved!</span>');
|
||
setTimeout(function() {
|
||
location.reload();
|
||
}, 1000);
|
||
},
|
||
error: function(xhr) {
|
||
var error = '';
|
||
try { error = JSON.parse(xhr.responseText).error; } catch(e) {}
|
||
msgEl.html('<span class="error-msg">Error: ' + error + '</span>');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Delete current decider
|
||
function deleteCurrentDecider() {
|
||
if (!currentDeciderKey) return;
|
||
var deciderName = $('#formName').val();
|
||
$('#deleteDeciderName').text(deciderName + ' (' + currentDeciderKey + ')');
|
||
$('#deleteModal').modal('show');
|
||
}
|
||
|
||
function confirmDelete() {
|
||
var key = currentDeciderKey;
|
||
$.ajax({
|
||
url: '/decider/admin/deciders/' + encodeURIComponent(key),
|
||
method: 'DELETE',
|
||
success: function() {
|
||
location.reload();
|
||
},
|
||
error: function() {
|
||
alert('Failed to delete decider');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Копирование ссылки на десайдер в буфер обмена.
|
||
// navigator.clipboard доступен только в защищённом контексте (https/localhost),
|
||
// поэтому есть запасной вариант через скрытую textarea.
|
||
function copyDeciderUrl() {
|
||
var url = $('#formKeyUrl').text();
|
||
if (!url) return;
|
||
|
||
function done(ok) {
|
||
var btn = $('#btnCopyUrl');
|
||
btn.text(ok ? 'Copied!' : 'Copy failed');
|
||
setTimeout(function() { btn.text('Copy'); }, 1500);
|
||
}
|
||
|
||
function fallbackCopy() {
|
||
var textarea = $('<textarea>').val(url).css({
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
opacity: 0
|
||
}).appendTo('body');
|
||
textarea[0].select();
|
||
var ok = false;
|
||
try {
|
||
ok = document.execCommand('copy');
|
||
} catch (e) {
|
||
ok = false;
|
||
}
|
||
textarea.remove();
|
||
done(ok);
|
||
}
|
||
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(url).then(function() {
|
||
done(true);
|
||
}, fallbackCopy);
|
||
} else {
|
||
fallbackCopy();
|
||
}
|
||
}
|
||
|
||
// Initialize: if there's only one decider, open it automatically
|
||
$(document).ready(function() {
|
||
// Nothing to do on load - user selects from list
|
||
$('#mapPickerModal').on('hidden.bs.modal', function() {
|
||
pickerTargetRow = null;
|
||
});
|
||
|
||
$('#btnCopyUrl').on('click', copyDeciderUrl);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|