|
15 | 15 | """ |
16 | 16 |
|
17 | 17 | import os |
| 18 | +import json |
18 | 19 | import time |
19 | 20 | from html import escape |
20 | 21 |
|
|
38 | 39 | os.environ.setdefault(_k, _v) |
39 | 40 |
|
40 | 41 | import streamlit as st |
| 42 | +import streamlit.components.v1 as components |
41 | 43 |
|
42 | 44 | # Import the REAL logic. Nothing below reimplements it. |
43 | 45 | from PasswordGenerator import ( |
|
65 | 67 | """ |
66 | 68 | <style> |
67 | 69 | .stApp { background: #0d1117; } |
68 | | - .block-container { max-width: 760px; padding-top: 2.2rem; } |
| 70 | + .block-container { max-width: 760px; padding-top: 3.5rem; } |
69 | 71 | h1, h2, h3 { color: #e6edf3; letter-spacing: -0.01em; } |
70 | 72 | p, label, .stMarkdown { color: #adbac7; } |
71 | 73 | .tl-tag { |
|
172 | 174 | res = st.session_state.last_pw |
173 | 175 | if res: |
174 | 176 | st.markdown("### Result") |
175 | | - display = res["password"] if show_pw else "•" * len(res["password"]) |
176 | | - # Escape before injecting: generated passwords contain HTML-special chars |
177 | | - # like < > & " which would otherwise break the markup (InvalidCharacterError). |
178 | | - st.markdown(f'<div class="tl-pw">{escape(display)}</div>', unsafe_allow_html=True) |
179 | | - st.markdown('<div class="tl-note">Triple-click to select · nothing is stored server-side</div>', |
180 | | - unsafe_allow_html=True) |
| 177 | + # Shown value is masked when "Show password" is off, but the Copy button |
| 178 | + # always copies the real password. Escape the shown text (passwords contain |
| 179 | + # < > & ") and pass the real value to JS safely via json.dumps. |
| 180 | + shown = escape(res["password"]) if show_pw else "•" * len(res["password"]) |
| 181 | + pw_js = json.dumps(res["password"]) |
| 182 | + # Two lines max for a 64-char password at this font size. |
| 183 | + box_height = 150 if len(res["password"]) > 40 else 120 |
| 184 | + components.html( |
| 185 | + f""" |
| 186 | + <div style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;"> |
| 187 | + <div style="display:flex; gap:10px; align-items:stretch;"> |
| 188 | + <div style="flex:1; color:#e6edf3; background:#161b22; border:1px solid #30363d; |
| 189 | + border-radius:10px; padding:16px 18px; font-size:22px; line-height:1.5; |
| 190 | + word-break:break-all; user-select:all;">{shown}</div> |
| 191 | + <button id="cp" style="flex:0 0 auto; cursor:pointer; color:#fff; background:#238636; |
| 192 | + border:0; border-radius:10px; padding:0 18px; font:600 14px ui-monospace,monospace;"> |
| 193 | + Copy |
| 194 | + </button> |
| 195 | + </div> |
| 196 | + <div style="font:12px ui-monospace,monospace; color:#768390; margin-top:8px;"> |
| 197 | + Copy button works whether or not the password is shown · nothing is stored server-side |
| 198 | + </div> |
| 199 | + </div> |
| 200 | + <script> |
| 201 | + const pw = {pw_js}; |
| 202 | + const btn = document.getElementById('cp'); |
| 203 | + btn.addEventListener('click', async () => {{ |
| 204 | + try {{ await navigator.clipboard.writeText(pw); }} |
| 205 | + catch (e) {{ |
| 206 | + const ta = document.createElement('textarea'); |
| 207 | + ta.value = pw; document.body.appendChild(ta); ta.select(); |
| 208 | + document.execCommand('copy'); ta.remove(); |
| 209 | + }} |
| 210 | + btn.textContent = 'Copied ✓'; btn.style.background = '#2ea043'; |
| 211 | + setTimeout(() => {{ btn.textContent = 'Copy'; btn.style.background = '#238636'; }}, 1500); |
| 212 | + }}); |
| 213 | + </script> |
| 214 | + """, |
| 215 | + height=box_height, |
| 216 | + ) |
181 | 217 |
|
182 | 218 | m1, m2, m3 = st.columns(3) |
183 | 219 | m1.markdown( |
|
0 commit comments