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

409 lines
12 KiB
C#

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>");
// === БЛОК: НАСТРОЙКИ ===
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 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;");
}