Spaces:
Paused
Paused
| """ | |
| ui.py — Interface navigateur pour CroxyProxy API | |
| Contient uniquement le HTML/CSS/JS de l'UI. Importé par app.py. | |
| """ | |
| from html import escape | |
| def error_page(message, url=""): | |
| """Page d'erreur affichée dans l'iframe quand une page ne charge pas.""" | |
| code = f'<p><code>{escape(url)}</code></p>' if url else '' | |
| return f"""<!doctype html><html><head><meta charset="utf-8"> | |
| <style>body{{background:#0d0f17;color:#e6e8f0;font-family:system-ui,sans-serif; | |
| display:flex;align-items:center;justify-content:center;height:100vh;margin:0}} | |
| .box{{text-align:center;max-width:480px;padding:2rem}} | |
| .icon{{font-size:3rem;margin-bottom:1rem}} | |
| h2{{margin:0 0 .5rem;font-weight:600}} | |
| p{{color:#8b90a3;line-height:1.5;margin:.4rem 0}} | |
| code{{background:#1a1d2b;padding:.2rem .5rem;border-radius:6px;font-size:.85rem; | |
| word-break:break-all}}</style> | |
| </head><body><div class="box"><div class="icon">⚠️</div> | |
| <h2>Impossible de charger la page</h2> | |
| <p>{escape(message)}</p> | |
| {code} | |
| <p style="margin-top:1.5rem;font-size:.85rem">Réessaie avec un autre serveur, ou un autre site.</p> | |
| </div></body></html>""" | |
| # Interface principale servie sur "/" | |
| BROWSER_UI = r"""<!doctype html> | |
| <html lang="fr"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Proxy Browser</title> | |
| <style> | |
| :root{ | |
| --bg:#0b0d14; --chrome:#12141f; --chrome-2:#171a28; | |
| --border:#232739; --border-2:#2d3247; | |
| --txt:#e7e9f3; --muted:#8b90a8; --faint:#5a5f78; | |
| --accent:#6d8bff; --accent-2:#8aa2ff; --ok:#4ade80; --err:#f87171; | |
| --radius:12px; | |
| } | |
| *{box-sizing:border-box} | |
| html,body{height:100%;margin:0} | |
| body{ | |
| background:var(--bg); color:var(--txt); | |
| font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif; | |
| display:flex; flex-direction:column; overflow:hidden; | |
| } | |
| .chrome{ | |
| background:linear-gradient(180deg,var(--chrome-2),var(--chrome)); | |
| border-bottom:1px solid var(--border); | |
| padding:10px 12px; display:flex; flex-direction:column; gap:9px; flex-shrink:0; | |
| } | |
| .row{display:flex; align-items:center; gap:8px} | |
| .nav-btn{ | |
| width:34px; height:34px; flex-shrink:0; | |
| background:var(--chrome-2); border:1px solid var(--border); | |
| border-radius:9px; color:var(--muted); cursor:pointer; | |
| font-size:15px; display:flex; align-items:center; justify-content:center; transition:.15s; | |
| } | |
| .nav-btn:hover{background:#1d2133; color:var(--txt); border-color:var(--border-2)} | |
| .nav-btn:active{transform:scale(.93)} | |
| .bar{ | |
| flex:1; display:flex; align-items:center; gap:8px; | |
| background:var(--bg); border:1px solid var(--border); | |
| border-radius:11px; padding:0 12px; height:38px; transition:.15s; | |
| } | |
| .bar:focus-within{border-color:var(--accent); box-shadow:0 0 0 3px rgba(109,139,255,.12)} | |
| .lock{font-size:13px; color:var(--faint); flex-shrink:0} | |
| #addr{flex:1; background:none; border:none; outline:none; color:var(--txt); | |
| font-size:14px; height:100%; min-width:0} | |
| #addr::placeholder{color:var(--faint)} | |
| .go{background:var(--accent); color:#fff; border:none; border-radius:10px; | |
| height:38px; padding:0 18px; font-size:14px; font-weight:600; cursor:pointer; | |
| flex-shrink:0; transition:.15s} | |
| .go:hover{background:var(--accent-2)} | |
| .go:active{transform:scale(.96)} | |
| .info{display:flex; align-items:center; gap:10px; font-size:12px; color:var(--muted); padding:0 2px} | |
| .pill{display:inline-flex; align-items:center; gap:6px; background:var(--chrome-2); | |
| border:1px solid var(--border); border-radius:20px; padding:4px 11px; font-size:12px} | |
| .dot{width:7px; height:7px; border-radius:50%; background:var(--faint)} | |
| .dot.ok{background:var(--ok); box-shadow:0 0 6px var(--ok)} | |
| .dot.err{background:var(--err)} | |
| .dot.load{background:var(--accent); animation:pulse 1s infinite} | |
| @keyframes pulse{50%{opacity:.3}} | |
| select{background:var(--chrome-2); border:1px solid var(--border); color:var(--txt); | |
| border-radius:20px; padding:4px 10px; font-size:12px; cursor:pointer; outline:none; max-width:220px} | |
| .info .spacer{flex:1} | |
| .info a{color:var(--faint); text-decoration:none} | |
| .info a:hover{color:var(--accent-2)} | |
| label.chk{display:inline-flex; align-items:center; gap:6px; cursor:pointer; user-select:none} | |
| label.chk input{accent-color:var(--accent)} | |
| .loader{height:2px; background:transparent; flex-shrink:0; overflow:hidden} | |
| .loader.active::after{content:""; display:block; height:100%; width:40%; | |
| background:linear-gradient(90deg,transparent,var(--accent),transparent); | |
| animation:slide 1s infinite linear} | |
| @keyframes slide{from{transform:translateX(-100%)}to{transform:translateX(350%)}} | |
| .viewport{flex:1; position:relative; background:#fff} | |
| iframe{width:100%; height:100%; border:none; background:#fff} | |
| .home{position:absolute; inset:0; background:var(--bg); display:flex; | |
| flex-direction:column; align-items:center; justify-content:center; | |
| gap:28px; padding:24px; overflow:auto} | |
| .home h1{font-size:30px; font-weight:700; margin:0; letter-spacing:-.5px; | |
| background:linear-gradient(120deg,var(--accent-2),#b794ff); | |
| -webkit-background-clip:text; background-clip:text; color:transparent} | |
| .home p{color:var(--muted); margin:0; font-size:14px} | |
| .shortcuts{display:grid; grid-template-columns:repeat(auto-fill,minmax(96px,1fr)); | |
| gap:12px; max-width:560px; width:100%} | |
| .sc{background:var(--chrome); border:1px solid var(--border); border-radius:var(--radius); | |
| padding:16px 10px; text-align:center; cursor:pointer; transition:.15s; color:var(--txt)} | |
| .sc:hover{border-color:var(--accent); transform:translateY(-2px); background:var(--chrome-2)} | |
| .sc .em{font-size:24px; display:block; margin-bottom:7px} | |
| .sc .nm{font-size:12px; color:var(--muted)} | |
| .hidden{display:none !important} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="chrome"> | |
| <div class="row"> | |
| <button class="nav-btn" id="back" title="Précédent">‹</button> | |
| <button class="nav-btn" id="fwd" title="Suivant">›</button> | |
| <button class="nav-btn" id="reload" title="Recharger">⟳</button> | |
| <button class="nav-btn" id="home" title="Accueil">⌂</button> | |
| <div class="bar"> | |
| <span class="lock">🔒</span> | |
| <input id="addr" placeholder="Cherche ou entre une URL…" autocomplete="off" spellcheck="false"> | |
| </div> | |
| <button class="go" id="go">Aller</button> | |
| </div> | |
| <div class="info"> | |
| <span class="pill"><span class="dot" id="dot"></span><span id="status">Prêt</span></span> | |
| <select id="srv" title="Serveur Croxy (API uniquement — la navigation est en direct)"><option value="">🔀 Serveurs (API)</option></select> | |
| <label class="chk"><input type="checkbox" id="rand"> Aléatoire</label> | |
| <span class="spacer"></span> | |
| <span id="ipinfo"></span> | |
| <a href="/api" target="_blank">API ↗</a> | |
| </div> | |
| </div> | |
| <div class="loader" id="loader"></div> | |
| <div class="viewport"> | |
| <iframe id="frame" referrerpolicy="no-referrer"></iframe> | |
| <div class="home" id="homescreen"> | |
| <div style="text-align:center"> | |
| <h1>Proxy Browser</h1> | |
| <p>Navigue sur le web via les serveurs CroxyProxy</p> | |
| </div> | |
| <div class="shortcuts" id="shortcuts"></div> | |
| <p style="font-size:12px;color:var(--faint);max-width:440px;text-align:center"> | |
| Récupération directe depuis le serveur. Les sites « server-side » | |
| (Wikipedia, vieux Reddit, docs, news) marchent bien ; les apps lourdes | |
| en JavaScript peuvent ne pas s'afficher correctement. | |
| </p> | |
| </div> | |
| </div> | |
| <script> | |
| const frame = document.getElementById('frame'); | |
| const addr = document.getElementById('addr'); | |
| const dot = document.getElementById('dot'); | |
| const statusEl= document.getElementById('status'); | |
| const loader = document.getElementById('loader'); | |
| const srvSel = document.getElementById('srv'); | |
| const randChk = document.getElementById('rand'); | |
| const ipinfo = document.getElementById('ipinfo'); | |
| const home = document.getElementById('homescreen'); | |
| let loadTimer = null; | |
| const SHORTCUTS = [ | |
| {nm:'Google', em:'🔍', u:'https://www.google.com'}, | |
| {nm:'DuckDuckGo', em:'🦆', u:'https://duckduckgo.com/html/'}, | |
| {nm:'Wikipedia', em:'📚', u:'https://www.wikipedia.org'}, | |
| {nm:'Reddit', em:'👽', u:'https://old.reddit.com'}, | |
| {nm:'YouTube', em:'▶️', u:'https://www.youtube.com'}, | |
| {nm:'GitHub', em:'🐙', u:'https://github.com'}, | |
| {nm:'HackerNews', em:'🟧', u:'https://news.ycombinator.com'}, | |
| {nm:'Mon IP', em:'🌐', u:'https://httpbin.org/ip'}, | |
| ]; | |
| const scWrap = document.getElementById('shortcuts'); | |
| SHORTCUTS.forEach(s=>{ | |
| const d = document.createElement('div'); | |
| d.className = 'sc'; | |
| d.innerHTML = '<span class="em">'+s.em+'</span><span class="nm">'+s.nm+'</span>'; | |
| d.onclick = ()=> navigate(s.u); | |
| scWrap.appendChild(d); | |
| }); | |
| // Liste des serveurs | |
| fetch('/servers').then(r=>r.json()).then(d=>{ | |
| (d.servers||[]).forEach(s=>{ | |
| const o = document.createElement('option'); | |
| o.value = s.id; | |
| o.textContent = (s.name||'?') + (s.ip ? ' ('+s.ip+')' : ''); | |
| srvSel.appendChild(o); | |
| }); | |
| }).catch(()=>{}); | |
| function setStatus(txt, kind){ statusEl.textContent = txt; dot.className = 'dot' + (kind ? ' '+kind : ''); } | |
| function showLoading(on){ loader.className = on ? 'loader active' : 'loader'; } | |
| function normalizeInput(v){ | |
| v = (v||'').trim(); | |
| if(!v) return null; | |
| if(/^https?:\/\//i.test(v)) return v; | |
| if(/^[\w-]+(\.[\w-]+)+(\/.*)?$/.test(v) && !v.includes(' ')) return 'https://' + v; | |
| return 'https://duckduckgo.com/html/?q=' + encodeURIComponent(v); | |
| } | |
| function chosenServer(){ | |
| if(randChk.checked && srvSel.options.length > 1){ | |
| const i = 1 + Math.floor(Math.random() * (srvSel.options.length - 1)); | |
| return srvSel.options[i].value; | |
| } | |
| return srvSel.value; | |
| } | |
| function navigate(url){ | |
| const target = normalizeInput(url); | |
| if(!target) return; | |
| const sid = chosenServer(); | |
| home.classList.add('hidden'); | |
| showLoading(true); | |
| setStatus('Chargement…', 'load'); | |
| addr.value = target; | |
| let view = '/view?url=' + encodeURIComponent(target); | |
| if(sid) view += '&server_id=' + sid; | |
| // garde-fou : si rien ne charge en 90s on prévient | |
| if(loadTimer) clearTimeout(loadTimer); | |
| loadTimer = setTimeout(()=>{ setStatus('Lent ou bloqué…', 'err'); showLoading(false); }, 90000); | |
| frame.src = view; | |
| } | |
| frame.addEventListener('load', ()=>{ | |
| showLoading(false); | |
| if(loadTimer){ clearTimeout(loadTimer); loadTimer = null; } | |
| try{ | |
| const loc = frame.contentWindow.location; | |
| if(loc.pathname === '/view'){ | |
| const params = new URLSearchParams(loc.search); | |
| const real = params.get('url'); | |
| if(real) addr.value = real; | |
| const sid = params.get('server_id'); | |
| const opt = sid ? [...srvSel.options].find(o=>o.value===sid) : null; | |
| ipinfo.textContent = opt ? (opt.textContent.match(/\(([^)]+)\)/)?.[1] || '') : ''; | |
| } | |
| setStatus('Connecté', 'ok'); | |
| }catch(e){ | |
| setStatus('Connecté', 'ok'); | |
| } | |
| }); | |
| document.getElementById('go').onclick = ()=> navigate(addr.value); | |
| document.getElementById('reload').onclick = ()=> { try{ frame.contentWindow.location.reload(); }catch(e){} }; | |
| document.getElementById('back').onclick = ()=> { try{ frame.contentWindow.history.back(); }catch(e){} }; | |
| document.getElementById('fwd').onclick = ()=> { try{ frame.contentWindow.history.forward(); }catch(e){} }; | |
| document.getElementById('home').onclick = ()=> { | |
| home.classList.remove('hidden'); addr.value = ''; | |
| setStatus('Prêt', ''); ipinfo.textContent = ''; frame.src = 'about:blank'; | |
| }; | |
| addr.addEventListener('keydown', e=>{ if(e.key === 'Enter') navigate(addr.value); }); | |
| addr.addEventListener('focus', ()=> addr.select()); | |
| </script> | |
| </body> | |
| </html>""" | |