Improve Web UI with footer, icon buttons, and better modals
Some checks failed
Validate / validate (push) Failing after 7s
Some checks failed
Validate / validate (push) Failing after 7s
- Add footer with author info (name, email, git repository link) - Replace device action buttons with icons to save space: - Start/Stop: ▶️/⏹️, Settings: ⚙️, Calibrate: 📐, Remove: 🗑️ - Added hover tooltips with translated text - Added btn-icon CSS class for compact styling - Replace native browser confirm() with custom modal dialog: - Matches app theme and supports translations - Used for logout and device removal confirmations - Added confirm.title, confirm.yes, confirm.no translations - Disable background scrolling when modals are open: - Added modal-open class to body when any modal opens - Prevents page scroll behind modals for better UX - Applied to all modals: login, settings, calibration, confirmation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -464,22 +464,22 @@ function createDeviceCard(device) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
${isProcessing ? `
|
${isProcessing ? `
|
||||||
<button class="btn btn-danger" onclick="stopProcessing('${device.id}')">
|
<button class="btn btn-icon btn-danger" onclick="stopProcessing('${device.id}')" title="${t('device.button.stop')}">
|
||||||
${t('device.button.stop')}
|
⏹️
|
||||||
</button>
|
</button>
|
||||||
` : `
|
` : `
|
||||||
<button class="btn btn-primary" onclick="startProcessing('${device.id}')">
|
<button class="btn btn-icon btn-primary" onclick="startProcessing('${device.id}')" title="${t('device.button.start')}">
|
||||||
${t('device.button.start')}
|
▶️
|
||||||
</button>
|
</button>
|
||||||
`}
|
`}
|
||||||
<button class="btn btn-secondary" onclick="showSettings('${device.id}')">
|
<button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
|
||||||
${t('device.button.settings')}
|
⚙️
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" onclick="showCalibration('${device.id}')">
|
<button class="btn btn-icon btn-secondary" onclick="showCalibration('${device.id}')" title="${t('device.button.calibrate')}">
|
||||||
${t('device.button.calibrate')}
|
📐
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" onclick="removeDevice('${device.id}')">
|
<button class="btn btn-icon btn-danger" onclick="removeDevice('${device.id}')" title="${t('device.button.remove')}">
|
||||||
${t('device.button.remove')}
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -542,7 +542,8 @@ async function stopProcessing(deviceId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeDevice(deviceId) {
|
async function removeDevice(deviceId) {
|
||||||
if (!confirm('Are you sure you want to remove this device?')) {
|
const confirmed = await showConfirm(t('device.remove.confirm'));
|
||||||
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,6 +604,7 @@ async function showSettings(deviceId) {
|
|||||||
// Show modal
|
// Show modal
|
||||||
const modal = document.getElementById('device-settings-modal');
|
const modal = document.getElementById('device-settings-modal');
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
|
||||||
// Focus first input
|
// Focus first input
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -620,6 +622,7 @@ function closeDeviceSettingsModal() {
|
|||||||
const error = document.getElementById('settings-error');
|
const error = document.getElementById('settings-error');
|
||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
error.style.display = 'none';
|
error.style.display = 'none';
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveDeviceSettings() {
|
async function saveDeviceSettings() {
|
||||||
@@ -742,6 +745,40 @@ function showToast(message, type = 'info') {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Confirmation modal
|
||||||
|
let confirmResolve = null;
|
||||||
|
|
||||||
|
function showConfirm(message, title = null) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
confirmResolve = resolve;
|
||||||
|
|
||||||
|
const modal = document.getElementById('confirm-modal');
|
||||||
|
const titleEl = document.getElementById('confirm-title');
|
||||||
|
const messageEl = document.getElementById('confirm-message');
|
||||||
|
const yesBtn = document.getElementById('confirm-yes-btn');
|
||||||
|
const noBtn = document.getElementById('confirm-no-btn');
|
||||||
|
|
||||||
|
titleEl.textContent = title || t('confirm.title');
|
||||||
|
messageEl.textContent = message;
|
||||||
|
yesBtn.textContent = t('confirm.yes');
|
||||||
|
noBtn.textContent = t('confirm.no');
|
||||||
|
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeConfirmModal(result) {
|
||||||
|
const modal = document.getElementById('confirm-modal');
|
||||||
|
modal.style.display = 'none';
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
|
||||||
|
if (confirmResolve) {
|
||||||
|
confirmResolve(result);
|
||||||
|
confirmResolve = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calibration functions
|
// Calibration functions
|
||||||
async function showCalibration(deviceId) {
|
async function showCalibration(deviceId) {
|
||||||
try {
|
try {
|
||||||
@@ -788,6 +825,7 @@ async function showCalibration(deviceId) {
|
|||||||
// Show modal
|
// Show modal
|
||||||
const modal = document.getElementById('calibration-modal');
|
const modal = document.getElementById('calibration-modal');
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load calibration:', error);
|
console.error('Failed to load calibration:', error);
|
||||||
@@ -800,6 +838,7 @@ function closeCalibrationModal() {
|
|||||||
const error = document.getElementById('calibration-error');
|
const error = document.getElementById('calibration-error');
|
||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
error.style.display = 'none';
|
error.style.display = 'none';
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCalibrationPreview() {
|
function updateCalibrationPreview() {
|
||||||
|
|||||||
@@ -86,6 +86,16 @@
|
|||||||
<button type="submit" class="btn btn-primary" data-i18n="device.button.add">Add Device</button>
|
<button type="submit" class="btn btn-primary" data-i18n="device.button.add">Add Device</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<footer class="app-footer">
|
||||||
|
<div class="footer-content">
|
||||||
|
<p>
|
||||||
|
Created by <strong>Alexei Dolgolyov</strong>
|
||||||
|
• <a href="mailto:dolgolyov.alexei@gmail.com">dolgolyov.alexei@gmail.com</a>
|
||||||
|
• <a href="https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed" target="_blank" rel="noopener">Source Code</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toast" class="toast"></div>
|
<div id="toast" class="toast"></div>
|
||||||
@@ -285,6 +295,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirmation Modal -->
|
||||||
|
<div id="confirm-modal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 450px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="confirm-title">Confirm Action</h2>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="confirm-message" class="modal-description"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" id="confirm-no-btn" onclick="closeConfirmModal(false)">No</button>
|
||||||
|
<button class="btn btn-danger" id="confirm-yes-btn" onclick="closeConfirmModal(true)">Yes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Initialize theme
|
// Initialize theme
|
||||||
@@ -338,17 +364,20 @@
|
|||||||
document.getElementById('modal-cancel-btn').style.display = 'inline-block';
|
document.getElementById('modal-cancel-btn').style.display = 'inline-block';
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
async function logout() {
|
||||||
if (confirm(t('auth.logout.confirm'))) {
|
const confirmed = await showConfirm(t('auth.logout.confirm'));
|
||||||
localStorage.removeItem('wled_api_key');
|
if (!confirmed) {
|
||||||
apiKey = null;
|
return;
|
||||||
updateAuthUI();
|
|
||||||
showToast(t('auth.logout.success'), 'info');
|
|
||||||
|
|
||||||
// Clear the UI
|
|
||||||
document.getElementById('devices-list').innerHTML = `<div class="loading">${t('auth.please_login')} devices</div>`;
|
|
||||||
document.getElementById('displays-list').innerHTML = `<div class="loading">${t('auth.please_login')} displays</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem('wled_api_key');
|
||||||
|
apiKey = null;
|
||||||
|
updateAuthUI();
|
||||||
|
showToast(t('auth.logout.success'), 'info');
|
||||||
|
|
||||||
|
// Clear the UI
|
||||||
|
document.getElementById('devices-list').innerHTML = `<div class="loading">${t('auth.please_login')} devices</div>`;
|
||||||
|
document.getElementById('displays-list').innerHTML = `<div class="loading">${t('auth.please_login')} displays</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on load
|
// Initialize on load
|
||||||
@@ -382,6 +411,7 @@
|
|||||||
input.placeholder = 'Enter your API key...';
|
input.placeholder = 'Enter your API key...';
|
||||||
error.style.display = 'none';
|
error.style.display = 'none';
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
|
||||||
// Hide cancel button if this is required login (no existing session)
|
// Hide cancel button if this is required login (no existing session)
|
||||||
cancelBtn.style.display = hideCancel ? 'none' : 'inline-block';
|
cancelBtn.style.display = hideCancel ? 'none' : 'inline-block';
|
||||||
@@ -392,6 +422,7 @@
|
|||||||
function closeApiKeyModal() {
|
function closeApiKeyModal() {
|
||||||
const modal = document.getElementById('api-key-modal');
|
const modal = document.getElementById('api-key-modal');
|
||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitApiKey() {
|
function submitApiKey() {
|
||||||
|
|||||||
@@ -112,5 +112,8 @@
|
|||||||
"server.offline": "Server offline",
|
"server.offline": "Server offline",
|
||||||
"error.unauthorized": "Unauthorized - please login",
|
"error.unauthorized": "Unauthorized - please login",
|
||||||
"error.network": "Network error",
|
"error.network": "Network error",
|
||||||
"error.unknown": "An error occurred"
|
"error.unknown": "An error occurred",
|
||||||
|
"confirm.title": "Confirm Action",
|
||||||
|
"confirm.yes": "Yes",
|
||||||
|
"confirm.no": "No"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,5 +112,8 @@
|
|||||||
"server.offline": "Сервер офлайн",
|
"server.offline": "Сервер офлайн",
|
||||||
"error.unauthorized": "Не авторизован - пожалуйста, войдите",
|
"error.unauthorized": "Не авторизован - пожалуйста, войдите",
|
||||||
"error.network": "Сетевая ошибка",
|
"error.network": "Сетевая ошибка",
|
||||||
"error.unknown": "Произошла ошибка"
|
"error.unknown": "Произошла ошибка",
|
||||||
|
"confirm.title": "Подтверждение Действия",
|
||||||
|
"confirm.yes": "Да",
|
||||||
|
"confirm.no": "Нет"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ body {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.modal-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -218,6 +222,18 @@ section {
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
min-width: auto;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.display-card {
|
.display-card {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
@@ -620,6 +636,38 @@ input:-webkit-autofill:focus {
|
|||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.app-footer {
|
||||||
|
margin-top: 60px;
|
||||||
|
padding: 30px 0;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content strong {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content a:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.displays-grid,
|
.displays-grid,
|
||||||
.devices-grid {
|
.devices-grid {
|
||||||
|
|||||||
Reference in New Issue
Block a user