462 lines
14 KiB
C#
462 lines
14 KiB
C#
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("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """);
|
|
}
|
|
|
|
private static string EscapeAttr(string s) => Escape(s).Replace("'", "'");
|
|
} |