Spaces:
Running
Running
Enhance UI and share link functionality
Browse files0.2.26
Updated `README.md` with details on persistent storage, challenge mode, and version updates.
Adjusted tooltip margin in `_render_guess_form` for better spacing.
Refined `_game_over_content` to improve difficulty rendering and share URL handling:
- Replaced copy-to-clipboard button with robust implementation supporting modern and legacy browsers.
- Added safe encoding for share URLs using `json` and `html` modules.
- Introduced new HTML structure for share URL display with improved styling.
- Enhanced JavaScript logic for copying URLs with multiple fallbacks.
- Increased HTML component height to accommodate layout changes.
These updates improve user experience, accessibility, and cross-browser compatibility.
- README.md +4 -0
- battlewords/__init__.py +1 -1
- battlewords/ui.py +87 -26
README.md
CHANGED
|
@@ -121,8 +121,12 @@ docker run -p8501:8501 battlewords
|
|
| 121 |
- Persistent Storage: all game results saved locally for personal statistics without accounts.
|
| 122 |
- Challenge Mode: remote storage of challenge results, multi-user leaderboard, and shareable links.
|
| 123 |
|
|
|
|
|
|
|
|
|
|
| 124 |
-0.2.25
|
| 125 |
- Share challenge from expander
|
|
|
|
| 126 |
|
| 127 |
-0.2.24
|
| 128 |
- compress height
|
|
|
|
| 121 |
- Persistent Storage: all game results saved locally for personal statistics without accounts.
|
| 122 |
- Challenge Mode: remote storage of challenge results, multi-user leaderboard, and shareable links.
|
| 123 |
|
| 124 |
+
-02.26
|
| 125 |
+
- fix copy/share link button
|
| 126 |
+
|
| 127 |
-0.2.25
|
| 128 |
- Share challenge from expander
|
| 129 |
+
- fix incorrect guess overlap of guess box
|
| 130 |
|
| 131 |
-0.2.24
|
| 132 |
- compress height
|
battlewords/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
__version__ = "0.2.
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage"]
|
|
|
|
| 1 |
+
__version__ = "0.2.26"
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage"]
|
battlewords/ui.py
CHANGED
|
@@ -1182,7 +1182,7 @@ def _render_guess_form(state: GameState):
|
|
| 1182 |
text-align: left !important;
|
| 1183 |
max-width: 320px !important;
|
| 1184 |
line-height: 1.2;
|
| 1185 |
-
margin-bottom:
|
| 1186 |
}
|
| 1187 |
/* Nudge tooltip popover below the trigger when possible */
|
| 1188 |
div[data-testid="stTooltipPopover"] {
|
|
@@ -1427,7 +1427,7 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1427 |
|
| 1428 |
# Render difficulty line only if we have a value
|
| 1429 |
difficulty_html = (
|
| 1430 |
-
f'<
|
| 1431 |
if difficulty_value is not None else ""
|
| 1432 |
)
|
| 1433 |
|
|
@@ -1449,6 +1449,7 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1449 |
"</tr></thead>"
|
| 1450 |
f"<tbody>{''.join(word_rows)}"
|
| 1451 |
f"<tr><td colspan=\"3\"><h5 class=\"m-2\">Total: {state.score} <span style='font-size:1.25rem; color:#1d64c8;'> ⏱ {timer_str}</span></h5></td></tr>"
|
|
|
|
| 1452 |
"</tbody>"
|
| 1453 |
"</table>"
|
| 1454 |
)
|
|
@@ -1500,8 +1501,7 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1500 |
<div class="mb-2">Tier: <strong>{compute_tier(state.score)}</strong></div>
|
| 1501 |
<div class="mb-2">Game Mode: <strong>{state.game_mode}</strong></div>
|
| 1502 |
<div class="mb-2">Wordlist: <strong>{st.session_state.get('selected_wordlist', '')}</strong></div>
|
| 1503 |
-
<div class="mb-0">{table_html}</div>
|
| 1504 |
-
{difficulty_html}
|
| 1505 |
</div>
|
| 1506 |
</div>
|
| 1507 |
""",
|
|
@@ -1649,33 +1649,94 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1649 |
st.success("✅ Share link generated!")
|
| 1650 |
st.code(share_url, language=None)
|
| 1651 |
|
| 1652 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1653 |
components.html(
|
| 1654 |
f"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1655 |
<script>
|
| 1656 |
-
function
|
| 1657 |
-
|
| 1658 |
-
|
| 1659 |
-
|
| 1660 |
-
|
| 1661 |
-
|
| 1662 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1663 |
</script>
|
| 1664 |
-
<button onclick="copyToClipboard()" style="
|
| 1665 |
-
width: 100%;
|
| 1666 |
-
padding: 0.5rem 1rem;
|
| 1667 |
-
background: #1d64c8;
|
| 1668 |
-
color: white;
|
| 1669 |
-
border: none;
|
| 1670 |
-
border-radius: 0.5rem;
|
| 1671 |
-
cursor: pointer;
|
| 1672 |
-
font-size: 1rem;
|
| 1673 |
-
font-weight: bold;
|
| 1674 |
-
">
|
| 1675 |
-
📋 Copy Link
|
| 1676 |
-
</button>
|
| 1677 |
""",
|
| 1678 |
-
height=
|
| 1679 |
)
|
| 1680 |
|
| 1681 |
st.markdown("---")
|
|
|
|
| 1182 |
text-align: left !important;
|
| 1183 |
max-width: 320px !important;
|
| 1184 |
line-height: 1.2;
|
| 1185 |
+
margin-bottom: 35px;
|
| 1186 |
}
|
| 1187 |
/* Nudge tooltip popover below the trigger when possible */
|
| 1188 |
div[data-testid="stTooltipPopover"] {
|
|
|
|
| 1427 |
|
| 1428 |
# Render difficulty line only if we have a value
|
| 1429 |
difficulty_html = (
|
| 1430 |
+
f'<tr><td colspan=\"3\"><h5 class=\"m-2\">Word list difficulty: {difficulty_value:.2f}</h5></td></tr>'
|
| 1431 |
if difficulty_value is not None else ""
|
| 1432 |
)
|
| 1433 |
|
|
|
|
| 1449 |
"</tr></thead>"
|
| 1450 |
f"<tbody>{''.join(word_rows)}"
|
| 1451 |
f"<tr><td colspan=\"3\"><h5 class=\"m-2\">Total: {state.score} <span style='font-size:1.25rem; color:#1d64c8;'> ⏱ {timer_str}</span></h5></td></tr>"
|
| 1452 |
+
f"{difficulty_html}"
|
| 1453 |
"</tbody>"
|
| 1454 |
"</table>"
|
| 1455 |
)
|
|
|
|
| 1501 |
<div class="mb-2">Tier: <strong>{compute_tier(state.score)}</strong></div>
|
| 1502 |
<div class="mb-2">Game Mode: <strong>{state.game_mode}</strong></div>
|
| 1503 |
<div class="mb-2">Wordlist: <strong>{st.session_state.get('selected_wordlist', '')}</strong></div>
|
| 1504 |
+
<div class="mb-0">{table_html}</div>
|
|
|
|
| 1505 |
</div>
|
| 1506 |
</div>
|
| 1507 |
""",
|
|
|
|
| 1649 |
st.success("✅ Share link generated!")
|
| 1650 |
st.code(share_url, language=None)
|
| 1651 |
|
| 1652 |
+
# More robust copy-to-clipboard implementation with fallbacks
|
| 1653 |
+
import json as _json
|
| 1654 |
+
import html as _html
|
| 1655 |
+
|
| 1656 |
+
_share_url_json = _json.dumps(share_url) # safe for JS
|
| 1657 |
+
_share_url_attr = _html.escape(share_url, quote=True) # safe for HTML attribute
|
| 1658 |
+
_share_url_text = _html.escape(share_url)
|
| 1659 |
+
|
| 1660 |
components.html(
|
| 1661 |
f"""
|
| 1662 |
+
<div id="bw-copy-container" style="
|
| 1663 |
+
display:flex;
|
| 1664 |
+
gap:8px;
|
| 1665 |
+
width:100%;
|
| 1666 |
+
align-items:center;
|
| 1667 |
+
margin-top:6px;
|
| 1668 |
+
justify-content:center;
|
| 1669 |
+
">
|
| 1670 |
+
|
| 1671 |
+
<strong><a href="{_share_url_attr}"
|
| 1672 |
+
target="_blank"
|
| 1673 |
+
rel="noopener noreferrer"
|
| 1674 |
+
style="text-decoration: underline; color: #fff; word-break: break-all; filter: drop-shadow(1px 1px 2px #003);">
|
| 1675 |
+
{_share_url_text}
|
| 1676 |
+
</a></strong>
|
| 1677 |
+
|
| 1678 |
+
|
| 1679 |
+
</div>
|
| 1680 |
<script>
|
| 1681 |
+
(function() {{
|
| 1682 |
+
const SHARE_URL = {_share_url_json};
|
| 1683 |
+
const input = document.getElementById('bw-share-url');
|
| 1684 |
+
|
| 1685 |
+
async function modernCopy(text) {{
|
| 1686 |
+
if (!navigator.clipboard || !window.isSecureContext) {{
|
| 1687 |
+
throw new Error('Clipboard API unavailable in this context');
|
| 1688 |
+
}}
|
| 1689 |
+
await navigator.clipboard.writeText(text);
|
| 1690 |
+
}}
|
| 1691 |
+
|
| 1692 |
+
function legacyCopy(text) {{
|
| 1693 |
+
const ta = document.createElement('textarea');
|
| 1694 |
+
ta.value = text;
|
| 1695 |
+
ta.setAttribute('readonly', '');
|
| 1696 |
+
ta.style.position = 'fixed';
|
| 1697 |
+
ta.style.top = '-1000px';
|
| 1698 |
+
ta.style.left = '-1000px';
|
| 1699 |
+
document.body.appendChild(ta);
|
| 1700 |
+
ta.focus();
|
| 1701 |
+
ta.select();
|
| 1702 |
+
let ok = false;
|
| 1703 |
+
try {{
|
| 1704 |
+
ok = document.execCommand('copy');
|
| 1705 |
+
}} finally {{
|
| 1706 |
+
document.body.removeChild(ta);
|
| 1707 |
+
}}
|
| 1708 |
+
if (!ok) throw new Error('execCommand failed');
|
| 1709 |
+
}}
|
| 1710 |
+
|
| 1711 |
+
btn.addEventListener('click', async () => {{
|
| 1712 |
+
try {{
|
| 1713 |
+
try {{
|
| 1714 |
+
await modernCopy(SHARE_URL);
|
| 1715 |
+
}} catch (e) {{
|
| 1716 |
+
legacyCopy(SHARE_URL);
|
| 1717 |
+
}}
|
| 1718 |
+
setStatus('Copied!');
|
| 1719 |
+
}} catch (e) {{
|
| 1720 |
+
// Final fallback: select input text and ask user to copy
|
| 1721 |
+
try {{
|
| 1722 |
+
input.removeAttribute('readonly');
|
| 1723 |
+
input.focus();
|
| 1724 |
+
input.select();
|
| 1725 |
+
input.setSelectionRange(0, input.value.length);
|
| 1726 |
+
document.execCommand('copy'); // may still work on some browsers
|
| 1727 |
+
input.setAttribute('readonly', '');
|
| 1728 |
+
setStatus('Copied!');
|
| 1729 |
+
}} catch (err) {{
|
| 1730 |
+
input.focus();
|
| 1731 |
+
input.select();
|
| 1732 |
+
setStatus('Press Ctrl+C');
|
| 1733 |
+
}}
|
| 1734 |
+
}}
|
| 1735 |
+
}});
|
| 1736 |
+
}})();
|
| 1737 |
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1738 |
""",
|
| 1739 |
+
height=80
|
| 1740 |
)
|
| 1741 |
|
| 1742 |
st.markdown("---")
|