diff --git a/admin.php b/admin.php index 500d4f4..8841491 100644 --- a/admin.php +++ b/admin.php @@ -1047,7 +1047,7 @@ - + @@ -1163,7 +1163,7 @@ - +
@@ -2104,7 +2104,7 @@ function switchTab(name) { initAccordion('evenements-systeme', false); // ── Seed Stats ──────────────────────────────────────────────────────────────── - + (function() { const BASE = 'api/torrent_stats.php'; let seedRange = localStorage.getItem('seed_range') || '7d'; diff --git a/app.js b/app.js index c0a8c9f..2038db6 100644 --- a/app.js +++ b/app.js @@ -306,7 +306,7 @@ function ouvrirShareSheet(path, name) { maxDlInput.type = 'number'; maxDlInput.min = '1'; maxDlInput.placeholder = 'Illimité'; - maxDlInput.className = 'sheet-pwd-input'; + maxDlInput.className = 'sheet-pwd-input sheet-num-input'; maxDlInput.style.width = '50%'; // Bouton créer @@ -374,15 +374,18 @@ async function creerLienSheet(path, password, expiresStr, sheet, maxDownloads = copyBtn.addEventListener('click', async () => { let text = fullUrl; if (password) text += '\nMot de passe\u00a0: ' + password; - try { - await navigator.clipboard.writeText(text); + const ok = await copyText(text); + if (ok) { copyBtn.innerHTML = ' Copié\u00a0!'; copyBtn.classList.add('is-copied'); + showToast('Lien copié', 'success'); setTimeout(() => { copyBtn.innerHTML = svgCopy() + ' Copier le lien'; copyBtn.classList.remove('is-copied'); }, 2500); - } catch { prompt('Lien :', fullUrl); } + } else { + showToast('Copie impossible', 'error'); + } }); const closeBtn = creerElement('button', 'sheet-close-btn'); @@ -455,24 +458,87 @@ async function creerLien(path, password, expiresStr, container) { } } +async function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { /* fall through */ } + } + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0'; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + let ok = false; + try { ok = document.execCommand('copy'); } catch { /* ignore */ } + ta.remove(); + return ok; +} + +function promptModal({ title, label, type = 'text', placeholder = '', confirmLabel = 'Confirmer' }) { + return new Promise(resolve => { + const overlay = creerElement('div', 'prompt-modal-overlay'); + const card = creerElement('div', 'prompt-modal-card'); + + const titleEl = creerElement('div', 'prompt-modal-title'); + titleEl.textContent = title; + + const labelEl = creerElement('label', 'sheet-field-label'); + labelEl.textContent = label; + + const input = document.createElement('input'); + input.type = type; + input.placeholder = placeholder; + input.className = 'sheet-pwd-input'; + input.style.marginBottom = '1.2rem'; + + const actions = creerElement('div', 'prompt-modal-actions'); + const cancelBtn = creerElement('button', 'btn btn-ghost'); + cancelBtn.textContent = 'Annuler'; + const confirmBtn = creerElement('button', 'btn btn-accent'); + confirmBtn.textContent = confirmLabel; + actions.append(cancelBtn, confirmBtn); + + card.append(titleEl, labelEl, input, actions); + overlay.appendChild(card); + document.body.appendChild(overlay); + input.focus(); + + function close(value) { + overlay.remove(); + document.removeEventListener('keydown', onKey); + resolve(value); + } + + function onKey(e) { if (e.key === 'Escape') close(null); } + document.addEventListener('keydown', onKey); + overlay.addEventListener('click', e => { if (e.target === overlay) close(null); }); + cancelBtn.addEventListener('click', () => close(null)); + confirmBtn.addEventListener('click', () => close(input.value)); + input.addEventListener('keydown', e => { if (e.key === 'Enter') close(input.value); }); + }); +} + /** * Copie un lien dans le presse-papiers */ async function copierLien(url, btn) { - try { - await navigator.clipboard.writeText(url); + const ok = await copyText(url); + if (ok) { const orig = btn.innerHTML; btn.innerHTML = '✓ Copié !'; btn.classList.add('btn-success'); btn.classList.remove('btn-primary'); + showToast('Lien copié', 'success'); setTimeout(() => { btn.innerHTML = orig; btn.classList.remove('btn-success'); btn.classList.add('btn-primary'); }, 2000); - } catch { - // Fallback si clipboard API indisponible - prompt('Copie ce lien :', url); + } else { + showToast('Copie impossible', 'error'); } } @@ -545,24 +611,34 @@ async function copierInfoLien(url, hasPwd, pwd, btn) { text += '\nMot de passe : ' + pwd; } - try { - await navigator.clipboard.writeText(text); + const ok = await copyText(text); + if (ok) { const orig = btn.innerHTML; btn.innerHTML = '✓ Copié !'; + showToast('Lien copié', 'success'); setTimeout(() => { btn.innerHTML = orig; }, 2000); - } catch { - prompt('Copie ces infos :', text); + } else { + showToast('Copie impossible', 'error'); } } /** * Envoie un lien de partage par email via l'API - * Demande l'adresse email dans un prompt */ // eslint-disable-next-line no-unused-vars async function envoyerEmail(linkId) { - const email = prompt('Adresse email du destinataire :'); + const email = await promptModal({ + title: 'Envoyer le lien par email', + label: 'Adresse email du destinataire', + type: 'email', + placeholder: 'destinataire@exemple.com', + confirmLabel: 'Envoyer', + }); if (!email || !email.trim()) return; + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) { + showToast('Adresse email invalide', 'error'); + return; + } try { const resp = await fetch('/share/ctrl.php?cmd=send_email', { diff --git a/style.css b/style.css index 1d858e0..4b522b1 100644 --- a/style.css +++ b/style.css @@ -901,6 +901,19 @@ body::before { box-shadow: 0 0 0 3px var(--accent-soft); } +/* Champ numérique (max téléchargements) — retire le spinner natif + espace du bouton */ +.sheet-num-input { + margin-bottom: 1.15rem; + padding: 0 1rem; + -moz-appearance: textfield; + appearance: textfield; +} +.sheet-num-input::-webkit-outer-spin-button, +.sheet-num-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + .sheet-pwd-input::placeholder { color: var(--text-muted); font-size: .9rem; @@ -1088,6 +1101,49 @@ body::before { border-color: var(--border-strong); } +.prompt-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, .6); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + z-index: 900; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn .15s ease-out; +} + +.prompt-modal-card { + background: var(--bg-surface); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + padding: 1.5rem 1.5rem 1.25rem; + width: calc(100% - 2rem); + max-width: 380px; + box-shadow: 0 24px 64px rgba(0, 0, 0, .6); + /* Card centré par le flex du parent → l'anim ne doit PAS translater (sinon saut à la fin). */ + animation: promptPop .22s cubic-bezier(.34, 1.56, .64, 1); +} + +@keyframes promptPop { + from { opacity: 0; transform: scale(.92); } + to { opacity: 1; transform: scale(1); } +} + +.prompt-modal-title { + font-size: .95rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 1.1rem; +} + +.prompt-modal-actions { + display: flex; + gap: .6rem; + justify-content: flex-end; +} + /* ============================================================================ * DASHBOARD SYSTÈME * Section collapsible entre l'en-tête et les liens actifs