10-06-2025, 11:20 PM
(09-06-2025, 11:52 AM)lucky a écrit : slt
voilà version html améliorée
Code :<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Multi-Interfaces</title>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
background: #1a1a2e;
overflow: hidden;
}
/* Mode configuration */
.config-mode {
background: #1a1a2e !important;
overflow: auto !important;
}
.config-container {
display: none;
max-width: 600px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
}
.config-mode .config-container {
display: block;
}
.config-panel {
background: rgba(255,255,255,0.1);
padding: 30px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
h1 {
color: #4CAF50;
text-align: center;
margin-bottom: 30px;
}
.section {
background: rgba(0,0,0,0.3);
padding: 20px;
margin: 20px 0;
border-radius: 5px;
}
h2 {
color: #2196F3;
margin-top: 0;
font-size: 18px;
}
.interface-group {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 10px;
margin: 15px 0;
align-items: center;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #555;
border-radius: 5px;
background: #333;
color: white;
font-size: 16px;
box-sizing: border-box;
transition: border-color 0.3s;
}
input[type="text"]:focus, input[type="password"]:focus {
outline: none;
border-color: #4CAF50;
background: #444;
}
.remove-btn {
background: #f44336;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
min-height: 44px;
}
.remove-btn:hover {
background: #d32f2f;
}
.add-btn {
background: #2196F3;
color: white;
border: none;
padding: 12px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 20px 0;
width: 100%;
transition: background 0.3s;
}
.add-btn:hover {
background: #1976D2;
}
.save-btn {
background: #4CAF50;
color: white;
border: none;
padding: 14px 30px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin: 10px 0;
}
.save-btn:hover {
background: #45a049;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(76,175,80,0.3);
}
.export-btn {
background: #FF5722;
color: white;
border: none;
padding: 14px 30px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin: 10px 0;
}
.export-btn:hover {
background: #E64A19;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255,87,34,0.3);
}
.help-text {
font-size: 13px;
color: #aaa;
margin: 15px 0;
padding: 15px;
background: rgba(255,255,255,0.05);
border-radius: 5px;
line-height: 1.6;
}
.example {
font-family: 'Courier New', monospace;
background: rgba(255,255,255,0.1);
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
.layout-preview {
margin: 20px 0;
padding: 20px;
background: #2196F3;
color: white;
border-radius: 5px;
text-align: center;
font-weight: bold;
}
.message {
margin: 20px 0;
padding: 15px;
border-radius: 5px;
text-align: center;
display: none;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Mode Dashboard */
.dashboard-container {
display: none;
height: 100vh;
gap: 2px;
background: #222;
}
.dashboard-mode .dashboard-container {
display: grid;
}
/* Layouts responsifs */
.layout-1 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.layout-2 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
}
.layout-3 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
.layout-3 .frame-container:first-child {
grid-row: span 2;
}
.layout-4 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
/* Mobile responsive layouts */
@media (max-width: 768px) and (orientation: portrait) {
.layout-2, .layout-3, .layout-4 {
display: flex !important;
overflow-x: auto !important;
scroll-snap-type: x mandatory !important;
gap: 0 !important;
-webkit-overflow-scrolling: touch;
}
.frame-container {
flex: 0 0 100vw;
scroll-snap-align: start;
min-height: 100vh;
}
.mobile-indicator {
position: fixed;
bottom: 70px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 100;
background: rgba(0,0,0,0.7);
padding: 8px 12px;
border-radius: 20px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255,255,255,0.5);
transition: all 0.3s;
}
.dot.active {
background: white;
transform: scale(1.3);
}
}
@media (max-width: 768px) and (orientation: landscape) {
.layout-4 {
grid-template-columns: 1fr 1fr !important;
}
.layout-3 .frame-container:first-child {
grid-row: 1;
}
}
.frame-container {
position: relative;
overflow: hidden;
background: white;
}
.frame-wrapper {
width: 100%;
height: 100%;
transform-origin: top left;
transition: transform 0.3s ease;
}
iframe {
width: 100%;
height: 100%;
border: none;
background: white;
}
/* Header avec bouton fullscreen uniquement */
.frame-header {
position: absolute;
top: 5px;
left: 5px;
z-index: 10;
}
/* Bouton fullscreen */
.fullscreen-btn {
background: rgba(0,0,0,0.8);
color: white;
border: none;
padding: 8px 10px;
font-size: 16px;
border-radius: 3px;
cursor: pointer;
transition: background 0.2s;
min-width: 36px;
min-height: 36px;
}
.fullscreen-btn:hover {
background: rgba(0,0,0,0.95);
}
.fullscreen-btn.exit {
background: rgba(220,53,69,0.9);
}
.fullscreen-btn.exit:hover {
background: rgba(220,53,69,1);
}
/* Plein écran */
.frame-container.fullscreen-active {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 9999 !important;
display: block !important;
}
/* Contrôles de zoom draggables */
.zoom-controls {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0,0,0,0.8);
padding: 5px;
border-radius: 3px;
z-index: 10;
display: flex;
gap: 5px;
cursor: move;
user-select: none;
transition: box-shadow 0.2s;
}
.zoom-controls:hover {
background: rgba(0,0,0,0.95);
box-shadow: 0 2px 5px rgba(0,0,0,0.5);
}
.zoom-controls.dragging {
opacity: 0.8;
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
z-index: 1000;
}
.zoom-btn {
width: 30px;
height: 30px;
border: none;
background: #444;
color: white;
cursor: pointer;
border-radius: 3px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.zoom-btn:hover {
background: #666;
}
.zoom-value {
color: white;
font-size: 12px;
min-width: 45px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
/* Boutons globaux */
.global-controls {
display: none;
position: fixed;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.9);
padding: 10px 20px;
border-radius: 25px;
z-index: 100;
backdrop-filter: blur(10px);
}
.dashboard-mode .global-controls {
display: block;
}
.global-btn {
background: #4CAF50;
color: white;
border: none;
padding: 8px 15px;
margin: 0 5px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.global-btn:hover {
background: #45a049;
transform: translateY(-2px);
}
.config-btn {
background: #FF9800;
}
.config-btn:hover {
background: #F57C00;
}
/* Mobile optimizations */
@media (max-width: 768px) {
.zoom-controls {
display: none !important;
}
.frame-header {
font-size: 12px;
padding: 6px 10px;
}
.fullscreen-btn {
min-width: 44px;
min-height: 44px;
}
.global-controls {
bottom: 20px;
padding: 12px 16px;
}
.global-btn {
padding: 10px 18px;
font-size: 16px;
}
/* Auto-zoom mobile */
.frame-wrapper {
transform: scale(0.5) !important;
width: 200% !important;
height: 200% !important;
}
.frame-container.fullscreen-active .frame-wrapper {
transform: scale(1) !important;
width: 100% !important;
height: 100% !important;
}
}
/* Message d'erreur iframe */
.iframe-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(248,215,218,0.95);
color: #721c24;
padding: 20px;
border-radius: 8px;
text-align: center;
max-width: 80%;
z-index: 20;
display: none;
backdrop-filter: blur(5px);
}
.iframe-error h3 {
margin: 0 0 10px 0;
font-size: 16px;
}
.iframe-error p {
margin: 5px 0;
font-size: 14px;
}
.iframe-error code {
background: rgba(0,0,0,0.1);
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
display: inline-block;
margin: 5px 0;
}
.iframe-error a {
color: #0056b3;
text-decoration: none;
font-weight: bold;
}
.iframe-error a:hover {
text-decoration: underline;
}
/* Authentification */
.auth-section {
background: rgba(76,175,80,0.1);
border: 1px solid rgba(76,175,80,0.3);
}
.auth-help {
font-size: 12px;
color: #aaa;
margin-top: 10px;
}
/* Loading spinner */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: #4CAF50;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Welcome message */
.welcome-message {
text-align: center;
padding: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
margin-bottom: 30px;
color: white;
}
.welcome-message h2 {
color: white;
margin-bottom: 10px;
font-size: 28px;
}
.welcome-message p {
font-size: 16px;
opacity: 0.9;
}
/* URL validation indicator */
.url-status {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
}
.url-valid { color: #4CAF50; }
.url-invalid { color: #f44336; }
</style>
</head>
<body class="config-mode">
<!-- Mode Configuration -->
<div class="config-container">
<div class="config-panel">
<h1>?️ Dashboard Multi-Interfaces</h1>
<div id="welcomeMessage" class="welcome-message">
<h2>Bienvenue !</h2>
<p>Configurez vos interfaces pour créer votre dashboard personnalisé</p>
</div>
<div class="section auth-section">
<h2>? Sécurité du Dashboard (optionnel)</h2>
<label>Nom d'utilisateur :</label>
<input type="text" id="authUser" placeholder="admin">
<label>Mot de passe :</label>
<input type="password" id="authPass" placeholder="Mot de passe">
<p class="auth-help">Laissez vide pour désactiver l'authentification</p>
</div>
<div class="section">
<h2>? Configuration des Interfaces</h2>
<div class="help-text">
<strong>Formats acceptés :</strong><br>
• <span class="example">192.168.1.100</span> → IP simple<br>
• <span class="example">192.168.1.100:8080</span> → IP avec port<br>
• <span class="example">monserveur.local</span> → Domaine local<br>
• <span class="example">https://app.example.com</span> → URL complète<br>
• <span class="example">http://serveur.com:3000/dashboard</span> → URL avec chemin
</div>
<div id="interfacesList"></div>
<button class="add-btn" onclick="addInterface()">➕ Ajouter une interface</button>
</div>
<div id="layoutPreview" class="layout-preview"></div>
<button class="save-btn" onclick="showDashboard()">? Afficher le dashboard</button>
<button class="export-btn" onclick="exportConfig()">? Exporter la configuration</button>
<div id="message" class="message"></div>
</div>
</div>
<!-- Mode Dashboard -->
<div id="dashboardContainer" class="dashboard-container"></div>
<!-- Contrôles globaux -->
<div class="global-controls">
<button class="global-btn" onclick="resetAllZoom()">↺ Réinitialiser</button>
<button class="global-btn" onclick="zoomAll(-0.1)">➖ Réduire tout</button>
<button class="global-btn" onclick="zoomAll(0.1)">➕ Agrandir tout</button>
<button class="global-btn config-btn" onclick="showConfig()">⚙️ Configuration</button>
</div>
<!-- Indicateur mobile -->
<div class="mobile-indicator" id="mobileIndicator" style="display: none;"></div>
<script>
// Configuration par défaut
let config = {
interfaces: [
{ url: '', name: 'Interface 1' },
{ url: '', name: 'Interface 2' }
],
authUser: '',
authPass: ''
};
let zoomLevels = {};
let currentFullscreen = null;
let isAuthenticated = false;
// Charger la configuration sauvegardée
function loadConfig() {
const saved = localStorage.getItem('dashboardConfig');
if (saved) {
try {
config = JSON.parse(saved);
// Assurer au moins 2 interfaces
while (config.interfaces.length < 2) {
config.interfaces.push({ url: '', name: `Interface ${config.interfaces.length + 1}` });
}
} catch (e) {
console.error('Erreur chargement config:', e);
}
}
}
// Sauvegarder la configuration
function saveConfig() {
localStorage.setItem('dashboardConfig', JSON.stringify(config));
}
// Valider et normaliser une URL
function normalizeURL(url) {
if (!url) return '';
url = url.trim();
// Si pas de protocole, ajouter http://
if (!url.match(/^https?:\/\//)) {
url = 'http://' + url;
}
return url;
}
// Valider une URL
function validateURL(url) {
if (!url) return false;
try {
new URL(normalizeURL(url));
return true;
} catch (e) {
return false;
}
}
// Afficher la liste des interfaces
function renderInterfaces() {
const container = document.getElementById('interfacesList');
container.innerHTML = '';
config.interfaces.forEach((interface, index) => {
const div = document.createElement('div');
div.className = 'interface-group';
const urlInput = document.createElement('input');
urlInput.type = 'text';
urlInput.value = interface.url;
urlInput.placeholder = `URL Interface ${index + 1} (ex: 192.168.1.100:8080)`;
urlInput.oninput = (e) => {
config.interfaces[index].url = e.target.value;
updateLayoutPreview();
updateURLStatus(urlInput);
};
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.value = interface.name;
nameInput.placeholder = `Nom ${index + 1}`;
nameInput.oninput = (e) => {
config.interfaces[index].name = e.target.value || `Interface ${index + 1}`;
};
div.appendChild(urlInput);
div.appendChild(nameInput);
// Bouton supprimer (sauf pour les 2 premières)
if (index >= 2) {
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.textContent = '✕';
removeBtn.onclick = () => removeInterface(index);
div.appendChild(removeBtn);
}
container.appendChild(div);
// Validation initiale
updateURLStatus(urlInput);
});
updateLayoutPreview();
}
// Indicateur de validation d'URL
function updateURLStatus(input) {
// Supprimer l'ancien indicateur
const oldStatus = input.parentElement.querySelector('.url-status');
if (oldStatus) oldStatus.remove();
if (input.value) {
const status = document.createElement('span');
status.className = 'url-status';
if (validateURL(input.value)) {
status.className += ' url-valid';
status.textContent = '✓';
} else {
status.className += ' url-invalid';
status.textContent = '✗';
}
input.parentElement.style.position = 'relative';
input.parentElement.appendChild(status);
}
}
// Ajouter une interface
function addInterface() {
if (config.interfaces.length < 4) {
config.interfaces.push({
url: '',
name: `Interface ${config.interfaces.length + 1}`
});
renderInterfaces();
} else {
showMessage('Maximum 4 interfaces', 'error');
}
}
// Supprimer une interface
function removeInterface(index) {
config.interfaces.splice(index, 1);
renderInterfaces();
}
// Mettre à jour l'aperçu
function updateLayoutPreview() {
const preview = document.getElementById('layoutPreview');
const validInterfaces = config.interfaces.filter(i => i.url).length;
let layout = '';
switch(validInterfaces) {
case 1: layout = '?️ Plein écran'; break;
case 2: layout = '? 2 colonnes côte à côte'; break;
case 3: layout = '? 1 grande vue + 2 petites'; break;
case 4: layout = '⚏ Grille 2x2'; break;
default: layout = '❓ Configurez au moins une interface';
}
preview.innerHTML = `<strong>Disposition :</strong> ${validInterfaces} interface${validInterfaces > 1 ? 's' : ''} - ${layout}`;
}
// Afficher un message
function showMessage(text, type) {
const messageEl = document.getElementById('message');
messageEl.textContent = text;
messageEl.className = 'message ' + type;
messageEl.style.display = 'block';
setTimeout(() => {
messageEl.style.display = 'none';
}, 3000);
}
// Vérifier l'authentification
function checkAuth() {
if (!config.authUser || !config.authPass) {
return true; // Pas d'auth configurée
}
if (isAuthenticated) {
return true;
}
const user = prompt('Nom d\'utilisateur :');
const pass = prompt('Mot de passe :');
if (user === config.authUser && pass === config.authPass) {
isAuthenticated = true;
return true;
}
alert('Identifiants incorrects');
return false;
}
// Afficher le dashboard
function showDashboard() {
// Sauvegarder l'auth
config.authUser = document.getElementById('authUser').value;
config.authPass = document.getElementById('authPass').value;
// Valider les interfaces
const validInterfaces = config.interfaces.filter(i => i.url && validateURL(i.url));
if (validInterfaces.length === 0) {
showMessage('Configurez au moins une interface valide', 'error');
return;
}
// Sauvegarder
saveConfig();
// Vérifier l'auth
if (!checkAuth()) {
return;
}
// Construire le dashboard
buildDashboard(validInterfaces);
// Basculer en mode dashboard
document.body.className = 'dashboard-mode';
document.getElementById('welcomeMessage').style.display = 'none';
}
// Construire le dashboard
function buildDashboard(interfaces) {
const container = document.getElementById('dashboardContainer');
container.innerHTML = '';
container.className = `dashboard-container layout-${interfaces.length}`;
interfaces.forEach((interface, index) => {
const frameDiv = document.createElement('div');
frameDiv.className = 'frame-container';
frameDiv.id = `frame${index + 1}-container`;
frameDiv.innerHTML = `
<div class="frame-header">
<button class="fullscreen-btn" onclick="toggleFullscreen(${index + 1})" id="fullscreen-btn-${index + 1}">⛶</button>
</div>
<div class="zoom-controls" id="zoom-controls-${index + 1}">
<button class="zoom-btn" onclick="zoom(${index + 1}, -0.1)">−</button>
<span class="zoom-value" id="zoom${index + 1}">100%</span>
<button class="zoom-btn" onclick="zoom(${index + 1}, 0.1)">+</button>
</div>
<div class="frame-wrapper" id="wrapper${index + 1}">
<iframe
id="frame${index + 1}"
src="${normalizeURL(interface.url)}"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-downloads allow-presentation allow-top-navigation"
referrerpolicy="no-referrer"
></iframe>
</div>
<div class="iframe-error" id="error${index + 1}">
<h3>⚠️ Impossible de charger cette interface</h3>
<p>Cette interface ne peut pas être affichée dans une iframe.</p>
<p><small>Certains sites bloquent l'affichage dans des iframes pour des raisons de sécurité.</small></p>
<p style="margin-top: 15px;">
<a href="${normalizeURL(interface.url)}" target="_blank">Ouvrir dans un nouvel onglet →</a>
</p>
</div>
`;
container.appendChild(frameDiv);
// Initialiser le zoom
zoomLevels[index + 1] = 1;
// Gérer les erreurs
const iframe = frameDiv.querySelector('iframe');
iframe.addEventListener('error', () => {
document.getElementById(`error${index + 1}`).style.display = 'block';
});
// Vérifier le chargement après un délai
setTimeout(() => {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Si on peut accéder au document, c'est bon
} catch (e) {
// Erreur de cross-origin, possiblement bloqué
console.warn(`L'iframe ${index + 1} pourrait être bloquée`);
}
}, 3000);
});
// Rendre les contrôles de zoom draggables
setTimeout(makeZoomControlsDraggable, 100);
// Setup mobile si nécessaire
if (isMobile()) {
setupMobileNavigation(interfaces.length);
}
}
// Détection mobile
function isMobile() {
return window.innerWidth <= 768;
}
// Navigation mobile
function setupMobileNavigation(count) {
const indicator = document.getElementById('mobileIndicator');
indicator.style.display = 'flex';
indicator.innerHTML = '';
for (let i = 0; i < count; i++) {
const dot = document.createElement('div');
dot.className = 'dot';
if (i === 0) dot.classList.add('active');
indicator.appendChild(dot);
}
const container = document.getElementById('dashboardContainer');
container.addEventListener('scroll', () => {
const scrollLeft = container.scrollLeft;
const width = window.innerWidth;
const currentIndex = Math.round(scrollLeft / width);
document.querySelectorAll('.dot').forEach((dot, index) => {
dot.classList.toggle('active', index === currentIndex);
});
});
}
// Zoom
function zoom(frameNum, delta) {
zoomLevels[frameNum] = Math.max(0.2, Math.min(2, (zoomLevels[frameNum] || 1) + delta));
updateZoom(frameNum);
}
function updateZoom(frameNum) {
const wrapper = document.getElementById(`wrapper${frameNum}`);
const zoomDisplay = document.getElementById(`zoom${frameNum}`);
if (!wrapper || !zoomDisplay) return;
const scale = zoomLevels[frameNum];
wrapper.style.transform = `scale(${scale})`;
wrapper.style.width = `${100 / scale}%`;
wrapper.style.height = `${100 / scale}%`;
zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
}
function resetAllZoom() {
Object.keys(zoomLevels).forEach(frameNum => {
zoomLevels[frameNum] = 1;
updateZoom(frameNum);
});
}
function zoomAll(delta) {
Object.keys(zoomLevels).forEach(frameNum => {
zoom(parseInt(frameNum), delta);
});
}
// Plein écran
function toggleFullscreen(frameNum) {
const container = document.getElementById(`frame${frameNum}-container`);
const btn = document.getElementById(`fullscreen-btn-${frameNum}`);
if (!container) return;
if (currentFullscreen === frameNum) {
// Sortir du plein écran
container.classList.remove('fullscreen-active');
btn.textContent = '⛶';
btn.classList.remove('exit');
currentFullscreen = null;
// Réafficher les autres
document.querySelectorAll('.frame-container').forEach(fc => {
fc.style.display = '';
});
} else {
// Sortir du plein écran actuel si existe
if (currentFullscreen) {
toggleFullscreen(currentFullscreen);
}
// Entrer en plein écran
container.classList.add('fullscreen-active');
btn.textContent = '✕';
btn.classList.add('exit');
currentFullscreen = frameNum;
// Masquer les autres
document.querySelectorAll('.frame-container').forEach(fc => {
if (fc.id !== `frame${frameNum}-container`) {
fc.style.display = 'none';
}
});
}
}
// Rendre les contrôles de zoom draggables
function makeZoomControlsDraggable() {
document.querySelectorAll('.zoom-controls').forEach(controls => {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
controls.addEventListener('mousedown', dragStart);
controls.addEventListener('touchstart', handleTouch);
function handleTouch(e) {
const touch = e.touches[0];
dragStart({
clientX: touch.clientX,
clientY: touch.clientY,
target: e.target
});
}
function dragStart(e) {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
initialX = e.clientX - controls.offsetLeft;
initialY = e.clientY - controls.offsetTop;
controls.classList.add('dragging');
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
document.addEventListener('touchmove', handleTouchMove);
document.addEventListener('touchend', dragEnd);
}
function handleTouchMove(e) {
const touch = e.touches[0];
drag({
clientX: touch.clientX,
clientY: touch.clientY
});
}
function drag(e) {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
const container = controls.closest('.frame-container');
const maxX = container.offsetWidth - controls.offsetWidth - 5;
const maxY = container.offsetHeight - controls.offsetHeight - 5;
currentX = Math.max(5, Math.min(maxX, currentX));
currentY = Math.max(5, Math.min(maxY, currentY));
controls.style.left = currentX + 'px';
controls.style.top = currentY + 'px';
controls.style.right = 'auto';
}
function dragEnd() {
isDragging = false;
controls.classList.remove('dragging');
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', dragEnd);
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', dragEnd);
}
});
}
// Retour à la configuration
function showConfig() {
document.body.className = 'config-mode';
renderInterfaces();
// Restaurer les valeurs d'auth
document.getElementById('authUser').value = config.authUser || '';
document.getElementById('authPass').value = config.authPass || '';
}
// Exporter la configuration
function exportConfig() {
// Sauvegarder d'abord la config actuelle
config.authUser = document.getElementById('authUser').value;
config.authPass = document.getElementById('authPass').value;
saveConfig();
const dataStr = JSON.stringify(config, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = 'dashboard-config.json';
link.click();
showMessage('Configuration exportée !', 'success');
}
// Gestion Escape pour sortir du plein écran
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && currentFullscreen) {
toggleFullscreen(currentFullscreen);
}
});
// Gestion du redimensionnement
window.addEventListener('resize', () => {
if (isMobile() && document.body.className === 'dashboard-mode') {
const validInterfaces = config.interfaces.filter(i => i.url);
setupMobileNavigation(validInterfaces.length);
}
});
// Initialisation
window.onload = () => {
loadConfig();
renderInterfaces();
// Si config déjà présente, masquer le message de bienvenue
if (config.interfaces.some(i => i.url)) {
document.getElementById('welcomeMessage').style.display = 'none';
}
};
</script>
</body>
</html>
Merci lucky,
Cette nouvelle version scroll droite <>gauche pour tel fonctionne parfaitement.
Très bonnes idées que ces dashboards que j'utilise autant à la maison qu'à l'extérieur.
Très bonne soirée,
Merci
Routeur UxIx2 (Maison et CE) - Dimmer Robotdyn avec triac BTA40 - Sonde T° sur CE - 4 PV 400Wc sur 2 PowerStream