1. Настройки на сайте обновляются без кнопки отправить 2. Добавлена кнопка для начисления монет через сайт(пока заглушка)
311 lines
9.2 KiB
C#
311 lines
9.2 KiB
C#
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.Text;
|
||
|
||
public class HtmlBuilder
|
||
{
|
||
public static string GetSettingsPage(List<Setting> settings)
|
||
{
|
||
return Decorate(GetSettingsString(settings), 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)
|
||
{
|
||
var sb = new StringBuilder();
|
||
|
||
sb.AppendLine("<h1>Настройки фишки</h1>");
|
||
|
||
// блок монет
|
||
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>");
|
||
|
||
// JS для realtime отправки
|
||
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;
|
||
// range шлём с небольшой задержкой, чтобы не заспамить
|
||
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;
|
||
}
|
||
|
||
// number/text (int)
|
||
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(){
|
||
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 = 'Сервер недоступен';
|
||
}
|
||
}
|
||
|
||
addBtn.addEventListener('click', addCoins);
|
||
})();
|
||
</script>
|
||
|
||
<div id='globalStatus' class='status'></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;
|
||
}
|
||
|
||
// минимальная защита от мусора в html
|
||
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("'", "'");
|
||
}
|