chip-game/Assets/Scripts/Server/HtmlBuilder.cs

462 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
public class HtmlBuilder
{
// старый метод оставим, чтобы ничего не поломать
public static string GetSettingsPage(List<Setting> settings)
=> GetSettingsPage(settings, actions: null);
// новый метод
public static string GetSettingsPage(List<Setting> settings, List<SettingAction> actions)
{
return Decorate(GetSettingsString(settings, actions), title: "Настройки Фишки");
}
public static string GetLoginPage()
{
string content = @"
<div class='login-root'>
<h1>Вход</h1>
<div class='form-group'>
<label for='pass'>Пароль</label>
<input id='pass' type='password' autocomplete='current-password' />
</div>
<button id='loginBtn' type='button'>Войти</button>
<p id='loginStatus' style='margin-top:12px;'></p>
</div>
<script>
(function(){
const pass = document.getElementById('pass');
const btn = document.getElementById('loginBtn');
const st = document.getElementById('loginStatus');
async function doLogin(){
st.textContent = '';
try{
const r = await fetch('/api/login', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ password: pass.value || '' })
});
const t = await r.text();
if(!r.ok){
st.textContent = 'Ошибка входа';
try{
const j = JSON.parse(t);
if(j && j.message) st.textContent = j.message;
}catch(e){}
return;
}
window.location.href = '/';
}catch(e){
st.textContent = 'Сервер недоступен';
}
}
btn.addEventListener('click', doLogin);
pass.addEventListener('keydown', (e)=>{ if(e.key === 'Enter') doLogin(); });
})();
</script>
";
return Decorate(content, title: "Вход");
}
// старый метод оставим
public static string GetSettingsString(List<Setting> settings)
=> GetSettingsString(settings, actions: null);
// новый метод: настройки + действия (раздельные блоки)
public static string GetSettingsString(List<Setting> settings, List<SettingAction> actions)
{
var sb = new StringBuilder();
sb.AppendLine("<h1>Настройки фишки</h1>");
// === БЛОК: АКТИВНЫЕ ПАРАМЕТРЫ (CountersCollector.Counters) ===
sb.AppendLine(GetActiveCountersBlock());
// === БЛОК: НАСТРОЙКИ ===
sb.AppendLine(@"
<details class='block' open>
<summary class='block-title'>Настройки</summary>
<div class='block-body'>
");
// монеты внутри настроек
sb.AppendLine(@"
<div class='panel'>
<h2>Монеты</h2>
<div class='coins-row'>
<input id='coinsAmount' type='number' value='10' min='1' step='1' class='coins-input'/>
<button id='addCoinsBtn' type='button' class='coins-btn'>Добавить монеты</button>
</div>
<div id='coinsStatus' class='status'></div>
</div>
");
sb.AppendLine(" <div class='panel'>");
sb.AppendLine(" <h2>Параметры</h2>");
foreach (Setting setting in settings)
{
string view = setting.Type switch
{
SettingType.Bool => GetBoolView(setting),
SettingType.Float => GetFloatView(setting),
SettingType.String => GetStringView(setting),
SettingType.Int => GetIntView(setting),
_ => $"<p>{Escape(setting.Alias)}: (unknown)</p>"
};
sb.AppendLine(view);
}
sb.AppendLine(" </div>"); // panel
sb.AppendLine(@"
</div>
</details>
");
// === БЛОК: ДЕЙСТВИЯ ===
sb.AppendLine(@"
<details class='block'>
<summary class='block-title'>Действия</summary>
<div class='block-body'>
");
sb.AppendLine(GetActionsView(actions));
sb.AppendLine(@"
</div>
</details>
");
// JS: realtime settings + coins + actions
sb.AppendLine(@"
<script>
(function(){
const status = (txt)=>{
const el = document.getElementById('globalStatus');
if(!el) return;
el.textContent = txt || '';
if(txt){ setTimeout(()=>{ el.textContent=''; }, 1200); }
};
const debounceMap = new Map();
function debounce(key, fn, ms){
if(debounceMap.has(key)) clearTimeout(debounceMap.get(key));
debounceMap.set(key, setTimeout(fn, ms));
}
async function sendSetting(id, value){
try{
const r = await fetch('/api/setting', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ id, value })
});
if(!r.ok){
status('Ошибка применения');
return;
}
status('Сохранено');
}catch(e){
status('Сервер недоступен');
}
}
// === Настройки ===
document.querySelectorAll('[data-setting-id]').forEach(el=>{
const id = el.getAttribute('data-setting-id');
if(el.type === 'checkbox'){
el.addEventListener('change', ()=>{
sendSetting(id, el.checked ? 'on' : 'off');
});
return;
}
if(el.type === 'range'){
const valueSpanId = el.getAttribute('data-value-span');
const valueSpan = valueSpanId ? document.getElementById(valueSpanId) : null;
const apply = ()=>{
if(valueSpan) valueSpan.textContent = el.value;
debounce('rng_'+id, ()=>sendSetting(id, el.value), 120);
};
el.addEventListener('input', apply);
el.addEventListener('change', apply);
return;
}
if(el.type === 'radio'){
el.addEventListener('change', ()=>{
if(el.checked) sendSetting(id, el.value);
});
return;
}
el.addEventListener('change', ()=>{
sendSetting(id, el.value);
});
});
// === Монеты ===
const coinsAmount = document.getElementById('coinsAmount');
const addBtn = document.getElementById('addCoinsBtn');
const coinsStatus = document.getElementById('coinsStatus');
async function addCoins(){
if(!coinsAmount || !addBtn || !coinsStatus) return;
coinsStatus.textContent = '';
const v = parseInt(coinsAmount.value || '0', 10);
if(!Number.isFinite(v) || v <= 0){
coinsStatus.textContent = 'Введи число > 0';
return;
}
try{
const r = await fetch('/api/coins', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ amount: v })
});
if(!r.ok){
coinsStatus.textContent = 'Ошибка запроса';
return;
}
coinsStatus.textContent = 'Ок';
setTimeout(()=>{ coinsStatus.textContent=''; }, 1200);
}catch(e){
coinsStatus.textContent = 'Сервер недоступен';
}
}
if(addBtn) addBtn.addEventListener('click', addCoins);
// === Действия ===
document.querySelectorAll('[data-action-name]').forEach(btn=>{
btn.addEventListener('click', async ()=>{
const name = btn.getAttribute('data-action-name') || '';
if(!name) return;
try{
btn.disabled = true;
const r = await fetch('/api/action', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ name })
});
const t = await r.text();
if(!r.ok){
status('Ошибка действия');
try{
const j = JSON.parse(t);
if(j && j.message) status(j.message);
}catch(e){}
return;
}
status('Ок');
}catch(e){
status('Сервер недоступен');
}finally{
setTimeout(()=>{ btn.disabled = false; }, 200);
}
});
});
})();
</script>
<div id='globalStatus' class='status'></div>
");
return sb.ToString();
}
private static string GetActionsView(List<SettingAction> actions)
{
var sb = new StringBuilder();
if (actions == null || actions.Count == 0)
{
sb.AppendLine("<p class='muted'>Нет доступных действий</p>");
return sb.ToString();
}
// группировка по Group (если пусто — в "Другое")
var groups = actions
.Where(a => a != null && !string.IsNullOrWhiteSpace(a.Name))
.GroupBy(a => string.IsNullOrWhiteSpace(a.Group) ? "Другое" : a.Group);
foreach (var g in groups)
{
sb.AppendLine($"<div class='action-group'>");
sb.AppendLine($" <h3 class='action-group-title'>{Escape(g.Key)}</h3>");
sb.AppendLine(" <div class='actions-grid'>");
foreach (var a in g)
{
sb.AppendLine(
$" <button type='button' class='action-btn' data-action-name='{EscapeAttr(a.Name)}'>{Escape(a.Name)}</button>"
);
}
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
}
return sb.ToString();
}
private static string GetBoolView(Setting setting)
{
string name = setting.Id;
string id = name + "ID";
string isChecked = setting.BoolValue ? "checked" : "";
return $@"
<div class='form-group'>
<label for='{id}'>{Escape(setting.Alias)}</label>
<input type='checkbox' id='{id}' data-setting-id='{EscapeAttr(name)}' {isChecked}>
</div>";
}
private static string GetFloatView(Setting setting)
{
string name = setting.Id;
string id = name + "ID";
string spanId = id + "_VAL";
string min = setting.FloatMin.ToString(CultureInfo.InvariantCulture);
string max = setting.FloatMax.ToString(CultureInfo.InvariantCulture);
string value = setting.FloatValue.ToString(CultureInfo.InvariantCulture);
string step = setting.FloatStep.ToString(CultureInfo.InvariantCulture);
return $@"
<div class='form-group'>
<label for='{id}'>{Escape(setting.Alias)}: <span id='{spanId}'>{value}</span></label>
<input id='{id}' type='range' min='{min}' max='{max}' value='{value}' step='{step}'
data-setting-id='{EscapeAttr(name)}' data-value-span='{spanId}'>
</div>";
}
private static string GetStringView(Setting setting)
{
var sb = new StringBuilder();
sb.AppendLine("<div class='form-group'>");
sb.AppendLine($"<p>{Escape(setting.Alias)}</p>");
for (int i = 0; i < setting.AllowedValues.Count; i++)
{
string stringValue = setting.AllowedValues[i];
string name = setting.Id;
string id = name + "ID" + i;
bool isChecked = setting.StringValue == stringValue;
string Checked = isChecked ? "checked" : "";
sb.AppendLine("<div class='radio-row'>");
sb.AppendLine($"<label for='{EscapeAttr(id)}'>{Escape(stringValue)}</label>");
sb.AppendLine(
$"<input type='radio' id='{EscapeAttr(id)}' " +
$"name='{EscapeAttr(name)}' value='{EscapeAttr(stringValue)}' {Checked} data-setting-id='{EscapeAttr(name)}'>"
);
sb.AppendLine("</div>");
}
sb.AppendLine("</div>");
return sb.ToString();
}
private static string GetIntView(Setting setting)
{
string name = setting.Id;
string id = name + "ID";
return $@"
<div class='form-group'>
<label for='{id}'>{Escape(setting.Alias)}</label>
<input id='{id}' type='number' value='{setting.IntValue}' step='1'
data-setting-id='{EscapeAttr(name)}'>
</div>";
}
private static string Decorate(string content, string title)
{
string res = "<!DOCTYPE html>"
+ "<html lang='ru'>"
+ "<head>"
+ "<meta charset='utf-8'>"
+ "<meta name='viewport' content='width=device-width, initial-scale=1'>"
+ "<link rel='stylesheet' href='style.css'>"
+ $"<title>{Escape(title)}</title>"
+ "</head>"
+ "<body>";
res += content;
res += "</body></html>";
return res;
}
private static string GetActiveCountersBlock()
{
var sb = new StringBuilder();
sb.AppendLine(@"
<details class='block'>
<summary class='block-title'>Активные параметры</summary>
<div class='block-body'>
<div class='panel'>
<h2>Активные параметры</h2>
");
// CountersCollector.Counters : ConcurrentDictionary<string,string>
var dict = CountersCollector.Counters;
if (dict == null || dict.IsEmpty)
{
sb.AppendLine("<div class='status'>Нет данных</div>");
}
else
{
// Стабильный вывод: снимок + сортировка по ключу
var items = dict.ToArray()
.OrderBy(kv => kv.Key ?? "", StringComparer.Ordinal);
foreach (var kv in items)
{
string key = kv.Key ?? "";
string val = kv.Value ?? "";
sb.AppendLine(@"
<div class='form-group'>");
sb.AppendLine($" <label>{Escape(key)}</label>");
sb.AppendLine($" <div class='status'>{Escape(val)}</div>");
sb.AppendLine("</div>");
}
}
sb.AppendLine(@"
</div>
</div>
</details>
");
return sb.ToString();
}
private static string Escape(string s)
{
if (string.IsNullOrEmpty(s)) return "";
return s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
}
private static string EscapeAttr(string s) => Escape(s).Replace("'", "&#39;");
}