homepage/templates/index.html

244 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Домашняя страница</title>
<link rel="icon" href="/static/favicon.ico">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/static/main.289a6408.css">
<link rel="stylesheet" href="/static/flame.css">
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
</head>
<body style="--color-primary: #FFFDEA; --color-accent: #5c5c5c; --color-background: #1a1a1a;">
<div id="root">
<div class="Layout_Container__HIHX7">
<div style="display:flex; flex-direction:row; align-items:center; gap:18px; margin-bottom:12px; width:100%;">
<form id="google-search" style="display:flex; align-items:center; gap:6px; flex:1 1 0;" onsubmit="event.preventDefault(); window.open('https://www.google.com/search?q='+encodeURIComponent(this.elements.q.value),'_blank');">
<img src="https://www.google.com/favicon.ico" alt="Google" width="24" height="24"/>
<input type="text" name="q" class="SearchBar_SearchBar__MQiwu" placeholder="Поиск Google..." autocomplete="off" style="width:100%; min-width:0; font-size:1.2em;"/>
</form>
<form id="yandex-search" style="display:flex; align-items:center; gap:6px; flex:1 1 0;" onsubmit="event.preventDefault(); window.open('https://yandex.ru/search/?text='+encodeURIComponent(this.elements.q.value),'_blank');">
<img src="https://yandex.ru/favicon.ico" alt="Yandex" width="24" height="24"/>
<input type="text" name="q" class="SearchBar_SearchBar__MQiwu" placeholder="Поиск Яндекс..." autocomplete="off" style="width:100%; min-width:0; font-size:1.2em;"/>
</form>
</div>
<header class="Header_Header__GCJdR">
<span class="Header_HeaderMain__oLoBB">
<div style="display:flex; flex-direction:row; align-items:center; gap:12px;">
<span style="font-size:1.6em; color: var(--color-primary);">{{ current_time }}</span>
<span style="font-size:1.6em; color: var(--color-primary);">{{ now }}</span>
<span id="services-status-indicator" title="Статус сервисов" style="display:inline-block; width:18px; height:18px; border-radius:4px; margin-left:8px; background:#888; border:1.5px solid #444;"></span>
</div>
<div class="WeatherWidget_WeatherWidget__3XlYt" id="weather-area" style="cursor:pointer;">
<div>
{% if weather and weather.icon %}
<img src="{{ weather.icon if not weather.icon.startswith('//') else 'https:' + weather.icon }}" width="50" height="50" alt="icon"/>
{% else %}
<canvas id="weather-icon" width="50" height="50"></canvas>
{% endif %}
</div>
<div class="WeatherWidget_WeatherDetails__1y3CA">
{% if weather %}
<span>{{ weather.temp }}°C</span>
<span>{{ weather.cloud }}% облачность</span>
{% else %}
<span>--°C</span>
<span>--% облачность</span>
{% endif %}
</div>
</div>
</span>
</header>
<a href="/applications"><h2 class="SectionHeadline_SectionHeadline__xa0oJ">Applications</h2></a>
<div class="AppGrid_AppGrid__ZxiaC">
{% for app in applications %}
<a href="{{ app.url }}" target="_blank" rel="noreferrer" class="AppCard_AppCard__NPTM5">
<div class="AppCard_AppCardIcon__ThrUl">
<i data-feather="{{ app.icon_name }}" class="AppCard_CustomIcon__+Tc3I" width="32" height="32"></i>
</div>
<div class="AppCard_AppCardDetails__HgNoY">
<h5>{{ app.name }}</h5>
<span>{{ app.description }}</span>
</div>
</a>
{% endfor %}
</div>
<div class="Home_HomeSpace__--hVj"></div>
<a href="/bookmarks"><h2 class="SectionHeadline_SectionHeadline__xa0oJ">Bookmarks</h2></a>
<div class="BookmarkGrid_BookmarkGrid__i3Tj4">
{% for group in bookmarks %}
<div class="BookmarkCard_BookmarkCard__ooGTe">
<h3 class="">{{ group.group }}</h3>
<div class="BookmarkCard_Bookmarks__alLCe">
{% for link in group.links %}
<a href="{{ link.url }}" target="_blank" rel="noreferrer">
<div class="BookmarkCard_BookmarkIcon__yWkUU">
<i data-feather="{{ link.icon_name }}" class="BookmarkCard_CustomIcon__D7Xys" width="16" height="16"></i>
</div>{{ link.name }}
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<a class="Home_SettingsButton__DrUPz" href="#" id="edit-config-btn">
<svg viewBox="0 0 24 24" role="presentation" class="Icon_Icon__jzcrU"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" style="fill: var(--color-background);"></path></svg>
<span style="margin-left:8px;">Редактировать настройки</span>
</a>
</div>
<div id="config-modal" style="display:none; position:fixed; z-index:1000; left:0; top:0; width:100vw; height:100vh; background:rgba(0,0,0,0.7); align-items:center; justify-content:center;">
<div id="config-modal-content" style="background:#fff; color:#222; padding:24px; border-radius:8px; min-width:320px; max-width:90vw; max-height:90vh; display:flex; flex-direction:column; gap:12px; resize:both; overflow:auto; width:60vw; height:60vh;">
<h2>Редактировать config.yaml</h2>
<div style="flex:1 1 auto; display:flex; flex-direction:column; min-height:200px;">
<div id="monaco-editor" style="flex:1 1 auto; min-width:300px; min-height:200px; max-width:100%; max-height:100%; height:100%; border:1px solid #ccc;"></div>
</div>
<div style="display:flex; gap:16px; margin-top:8px;">
<button id="save-config-btn" style="padding:10px 24px; font-size:1.1em;">Сохранить</button>
<button id="cancel-config-btn" style="padding:10px 24px; font-size:1.1em;">Отмена</button>
</div>
<div id="config-error" style="color:red;"></div>
</div>
</div>
<div id="forecast-modal" style="display:none; position:fixed; z-index:2000; left:0; top:0; width:100vw; height:100vh; background:rgba(0,0,0,0.7); align-items:center; justify-content:center;">
<div style="background:#fff; color:#222; padding:24px; border-radius:8px; min-width:320px; max-width:90vw; max-height:90vh; display:flex; flex-direction:column; gap:12px;">
<h2 id="forecast-title">Прогноз</h2>
<div id="forecast-content" style="overflow-y:auto; max-height:60vh;"></div>
<button id="close-forecast-btn" style="padding:10px 24px; font-size:1.1em; align-self:flex-end;">Закрыть</button>
</div>
</div>
<div class="NotificationCenter_NotificationCenter__Tfgzh" style="height: 0px;"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js"></script>
<script>feather.replace()</script>
<script>
let monacoEditor;
const editBtn = document.getElementById('edit-config-btn');
const modal = document.getElementById('config-modal');
const modalContent = document.getElementById('config-modal-content');
const saveBtn = document.getElementById('save-config-btn');
const cancelBtn = document.getElementById('cancel-config-btn');
const errorDiv = document.getElementById('config-error');
function resizeMonaco() {
if (monacoEditor) {
const editorDiv = document.getElementById('monaco-editor');
const rect = editorDiv.getBoundingClientRect();
monacoEditor.layout({ width: rect.width, height: rect.height });
}
}
// Drag/resize поддерживается через CSS (resize:both)
editBtn.onclick = function(e) {
e.preventDefault();
fetch('/api/config').then(r => r.json()).then(data => {
errorDiv.textContent = '';
modal.style.display = 'flex';
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs' } });
require(['vs/editor/editor.main'], function () {
if (monacoEditor) {
monacoEditor.setValue(data.content);
setTimeout(resizeMonaco, 100);
} else {
monacoEditor = monaco.editor.create(document.getElementById('monaco-editor'), {
value: data.content,
language: 'yaml',
theme: 'vs-dark',
automaticLayout: false,
minimap: { enabled: false },
});
setTimeout(resizeMonaco, 100);
}
});
});
};
cancelBtn.onclick = function() {
modal.style.display = 'none';
};
saveBtn.onclick = function() {
const yamlText = monacoEditor.getValue();
if (!window.jsyaml || typeof window.jsyaml.load !== 'function') {
errorDiv.textContent = 'Ошибка: js-yaml не загружен. Проверьте подключение скрипта.';
return;
}
try {
window.jsyaml.load(yamlText); // Проверка синтаксиса
fetch('/api/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({content: yamlText})
}).then(r => r.json().then(data => ({ok: r.ok, data}))).then(res => {
if(res.ok) {
modal.style.display = 'none';
location.reload();
} else {
errorDiv.textContent = res.data.error || 'Ошибка сохранения';
}
});
} catch (e) {
errorDiv.textContent = 'Ошибка YAML: ' + e.message;
}
};
// Следим за изменением размера окна редактора
let resizeObserver = new ResizeObserver(() => {
resizeMonaco();
});
resizeObserver.observe(document.getElementById('config-modal-content'));
const weatherArea = document.getElementById('weather-area');
const forecastModal = document.getElementById('forecast-modal');
const forecastContent = document.getElementById('forecast-content');
const closeForecastBtn = document.getElementById('close-forecast-btn');
const forecastTitle = document.getElementById('forecast-title');
weatherArea.onclick = function() {
forecastContent.innerHTML = 'Загрузка...';
forecastModal.style.display = 'flex';
fetch('/api/weather-forecast').then(r => r.json()).then(data => {
if(data.error) {
forecastContent.innerHTML = '<span style="color:red;">'+data.error+'</span>';
forecastTitle.textContent = 'Прогноз';
return;
}
let days = data.forecast.length;
forecastTitle.textContent = `Прогноз на ${days} ${days === 1 ? 'день' : (days < 5 ? 'дня' : 'дней')}`;
let html = '<table style="width:100%; border-collapse:collapse;">';
html += '<tr><th style="text-align:left; padding:4px;">Дата</th><th style="text-align:left; padding:4px;">Температура</th><th style="text-align:left; padding:4px;">Облачность</th><th style="text-align:left; padding:4px;">Состояние</th><th></th></tr>';
for(const day of data.forecast) {
html += `<tr><td style="padding:4px;">${day.date}</td><td style="padding:4px;">${day.temp}°C</td><td style="padding:4px;">${day.cloud}%</td><td style="padding:4px;">${day.text||''}</td><td>${day.icon ? `<img src='${day.icon.startsWith('//') ? 'https:' + day.icon : day.icon}' width='32' height='32'/>` : ''}</td></tr>`;
}
html += '</table>';
forecastContent.innerHTML = html;
}).catch(() => {
forecastContent.innerHTML = '<span style="color:red;">Ошибка загрузки прогноза</span>';
forecastTitle.textContent = 'Прогноз';
});
};
closeForecastBtn.onclick = function() {
forecastModal.style.display = 'none';
};
window.addEventListener('DOMContentLoaded', function() {
const indicator = document.getElementById('services-status-indicator');
if (indicator) {
fetch('/api/services-status').then(r => r.json()).then(data => {
if (data.all_services_up === true) {
indicator.style.background = '#2ecc40';
} else if (data.all_services_up === false) {
indicator.style.background = '#ff4136';
} else {
indicator.style.background = '#888';
}
}).catch(() => {
indicator.style.background = '#888';
});
}
});
</script>
</body>
</html>