Skip to content

Commit 2b25fa8

Browse files
feat(utils): add utility modules for common functionality
Create utility modules: - Add text_processing.py for phrase splitting logic - Add javascript.py for JavaScript integration utilities - Add src/__init__.py package initialization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8cbabe3 commit 2b25fa8

File tree

4 files changed

+175
-0
lines changed

4 files changed

+175
-0
lines changed

src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Main application package

src/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Utilities package initialization

src/utils/javascript.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# JavaScript integration utilities
2+
import logging
3+
from nicegui import ui
4+
5+
def run_fitty_js():
6+
"""Run the fitty JavaScript library to resize text."""
7+
try:
8+
js_code = """
9+
setTimeout(function() {
10+
if (typeof fitty !== 'undefined') {
11+
fitty('.fit-text', { multiLine: true, minSize: 10, maxSize: 1000 });
12+
fitty('.fit-text-small', { multiLine: true, minSize: 10, maxSize: 72 });
13+
}
14+
}, 50);
15+
"""
16+
ui.run_javascript(js_code)
17+
except Exception as e:
18+
logging.debug(f"JavaScript execution failed (likely disconnected client): {e}")
19+
20+
def setup_javascript():
21+
"""Add required JavaScript libraries to the page."""
22+
# Add fitty.js for text fitting
23+
ui.add_head_html('<script src="https://cdn.jsdelivr.net/npm/fitty@2.3.6/dist/fitty.min.js"></script>')
24+
25+
# Add html2canvas for saving the board as image
26+
ui.add_head_html("""
27+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
28+
<script>
29+
function captureBoardAndDownload(seed) {
30+
var boardElem = document.getElementById('board-container');
31+
if (!boardElem) {
32+
alert("Board container not found!");
33+
return;
34+
}
35+
// Run fitty to ensure text is resized and centered
36+
if (typeof fitty !== 'undefined') {
37+
fitty('.fit-text', { multiLine: true, minSize: 10, maxSize: 1000 });
38+
fitty('.fit-text-small', { multiLine: true, minSize: 10, maxSize: 72 });
39+
}
40+
41+
// Wait a short period to ensure that the board is fully rendered
42+
setTimeout(function() {
43+
html2canvas(boardElem, {
44+
useCORS: true,
45+
scale: 10, // Increase scale for higher resolution
46+
logging: true,
47+
backgroundColor: null
48+
}).then(function(canvas) {
49+
var link = document.createElement('a');
50+
link.download = `bingo_board_${seed}.png`;
51+
link.href = canvas.toDataURL('image/png');
52+
link.click();
53+
});
54+
}, 500);
55+
}
56+
57+
// Function to safely apply fitty
58+
function applyFitty() {
59+
if (typeof fitty !== 'undefined') {
60+
fitty('.fit-text', { multiLine: true, minSize: 10, maxSize: 1000 });
61+
fitty('.fit-text-small', { multiLine: true, minSize: 10, maxSize: 72 });
62+
fitty('.fit-header', { multiLine: true, minSize: 10, maxSize: 2000 });
63+
}
64+
}
65+
</script>
66+
""")
67+
68+
# Add event listeners for responsive text resizing
69+
ui.add_head_html("""<script>
70+
// Run fitty when DOM is loaded
71+
document.addEventListener('DOMContentLoaded', function() {
72+
setTimeout(applyFitty, 100);
73+
});
74+
75+
// Run fitty when window is resized
76+
let resizeTimer;
77+
window.addEventListener('resize', function() {
78+
clearTimeout(resizeTimer);
79+
resizeTimer = setTimeout(applyFitty, 100);
80+
});
81+
82+
// Periodically check and reapply fitty for any dynamic changes
83+
setInterval(applyFitty, 1000);
84+
</script>""")

src/utils/text_processing.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Text processing utilities
2+
from typing import List
3+
4+
def split_phrase_into_lines(phrase: str, forced_lines: int = None) -> List[str]:
5+
"""
6+
Split a phrase into multiple lines for better visual display.
7+
"""
8+
words = phrase.split()
9+
n = len(words)
10+
11+
if n <= 3:
12+
return words
13+
14+
# Helper: total length of a list of words (including spaces between words)
15+
def segment_length(segment):
16+
return sum(len(word) for word in segment) + (len(segment) - 1 if segment else 0)
17+
18+
candidates = [] # list of tuples: (number_of_lines, diff, candidate)
19+
20+
# 2-line candidate
21+
best_diff_2 = float('inf')
22+
best_seg_2 = None
23+
for i in range(1, n):
24+
seg1 = words[:i]
25+
seg2 = words[i:]
26+
len1 = segment_length(seg1)
27+
len2 = segment_length(seg2)
28+
diff = abs(len1 - len2)
29+
if diff < best_diff_2:
30+
best_diff_2 = diff
31+
best_seg_2 = [" ".join(seg1), " ".join(seg2)]
32+
if best_seg_2 is not None:
33+
candidates.append((2, best_diff_2, best_seg_2))
34+
35+
# 3-line candidate (if at least 4 words)
36+
if n >= 4:
37+
best_diff_3 = float('inf')
38+
best_seg_3 = None
39+
for i in range(1, n-1):
40+
for j in range(i+1, n):
41+
seg1 = words[:i]
42+
seg2 = words[i:j]
43+
seg3 = words[j:]
44+
len1 = segment_length(seg1)
45+
len2 = segment_length(seg2)
46+
len3 = segment_length(seg3)
47+
current_diff = max(len1, len2, len3) - min(len1, len2, len3)
48+
if current_diff < best_diff_3:
49+
best_diff_3 = current_diff
50+
best_seg_3 = [" ".join(seg1), " ".join(seg2), " ".join(seg3)]
51+
if best_seg_3 is not None:
52+
candidates.append((3, best_diff_3, best_seg_3))
53+
54+
# 4-line candidate (if at least 5 words)
55+
if n >= 5:
56+
best_diff_4 = float('inf')
57+
best_seg_4 = None
58+
for i in range(1, n-2):
59+
for j in range(i+1, n-1):
60+
for k in range(j+1, n):
61+
seg1 = words[:i]
62+
seg2 = words[i:j]
63+
seg3 = words[j:k]
64+
seg4 = words[k:]
65+
len1 = segment_length(seg1)
66+
len2 = segment_length(seg2)
67+
len3 = segment_length(seg3)
68+
len4 = segment_length(seg4)
69+
diff = max(len1, len2, len3, len4) - min(len1, len2, len3, len4)
70+
if diff < best_diff_4:
71+
best_diff_4 = diff
72+
best_seg_4 = [" ".join(seg1), " ".join(seg2), " ".join(seg3), " ".join(seg4)]
73+
if best_seg_4 is not None:
74+
candidates.append((4, best_diff_4, best_seg_4))
75+
76+
# If a forced number of lines is specified, try to return that candidate first
77+
if forced_lines is not None:
78+
forced_candidates = [cand for cand in candidates if cand[0] == forced_lines]
79+
if forced_candidates:
80+
_, _, best_candidate = min(forced_candidates, key=lambda x: x[1])
81+
return best_candidate
82+
83+
# Otherwise, choose the candidate with the smallest diff
84+
if candidates:
85+
_, _, best_candidate = min(candidates, key=lambda x: x[1])
86+
return best_candidate
87+
else:
88+
# fallback (should never happen)
89+
return [" ".join(words)]

0 commit comments

Comments
 (0)