Compare commits
7 Commits
372e246124
...
0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 54de8672dc | |||
| 26108fe073 | |||
| fe8b93e8a8 | |||
| 3ffb2c5785 | |||
| 5b337e3168 | |||
| 8939c6ffb5 | |||
| 8d194c0dff |
26
README.md
26
README.md
@ -1,2 +1,28 @@
|
||||
# multiplayer_crosswords
|
||||
|
||||
This project is a web-based multiplayer crossword puzzle game that allows multiple users to collaborate in solving crossword puzzles in real-time. It features a user-friendly interface, session management, and real-time updates to enhance the collaborative experience.
|
||||
|
||||
## installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://the-cake-is-a-lie.net/gitea/jonas/multiplayer_crosswords.git
|
||||
cd multiplayer_crosswords
|
||||
```
|
||||
|
||||
2. Install this repository as a package:
|
||||
```bash
|
||||
pip install .
|
||||
```
|
||||
|
||||
## start the server
|
||||
|
||||
```bash
|
||||
python -m multiplayer_crosswords.server.main
|
||||
```
|
||||
|
||||
## start the webui
|
||||
|
||||
```bash
|
||||
python -m multiplayer_crosswords.server.serve_frontend
|
||||
```
|
||||
|
||||
@ -140,9 +140,9 @@ class Crossword:
|
||||
# Now find a solution word: generate a random word from the dictionary (with 10-20 letters)
|
||||
# and try to find the necessary letter positions in the grid. if we fail we repeat with another word (max 10 tries)
|
||||
solution_word_positions: Optional[List[Tuple[int, int]]] = None
|
||||
max_solution_word_attempts = 10
|
||||
for _ in range(max_solution_word_attempts):
|
||||
random_length = random.randint(10, 20)
|
||||
max_solution_word_attempts = 20
|
||||
for i in range(max_solution_word_attempts):
|
||||
random_length = random.randint(10, 27 - (i // 2))
|
||||
possible_words = dictionary.find_by_pattern('*' * random_length)
|
||||
if not possible_words:
|
||||
continue
|
||||
|
||||
35
multiplayer_crosswords/server/serve_frontend.py
Normal file
35
multiplayer_crosswords/server/serve_frontend.py
Normal file
@ -0,0 +1,35 @@
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
from http import server as http
|
||||
import http.server
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent / "webui"
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Serve Multiplayer Crossword Frontend")
|
||||
parser.add_argument("--host", type=str, default="0.0.0.0")
|
||||
parser.add_argument("--port", type=int, default=8000)
|
||||
parser.add_argument("--no-file-list", action="store_true", help="Disable directory listing")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
path = BASE_DIR
|
||||
host = args.host
|
||||
port = args.port
|
||||
no_file_list = args.no_file_list
|
||||
|
||||
class CustomHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=str(path), **kwargs)
|
||||
|
||||
if no_file_list:
|
||||
def list_directory(self, path):
|
||||
self.send_error(403, "Directory listing not allowed")
|
||||
return None
|
||||
server_address = (host, port)
|
||||
httpd = http.server.HTTPServer(server_address, CustomHandler)
|
||||
print(f"Serving frontend at http://{host}:{port}/ from {path}")
|
||||
httpd.serve_forever()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -3,8 +3,8 @@ from pydantic import BaseModel
|
||||
DEFAULT_WEBSOCKET_HOST = "0.0.0.0"
|
||||
DEFAULT_WEBSOCKET_PORT = 8765
|
||||
|
||||
DEFAULT_MIN_GRID_SIZE = 10
|
||||
DEFAULT_MAX_GRID_SIZE = 30
|
||||
DEFAULT_MIN_GRID_SIZE = 12
|
||||
DEFAULT_MAX_GRID_SIZE = 25
|
||||
|
||||
DEFAULT_GRID_BLOCK_RATIO = 0.39
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ class BoardSizePreset(str, Enum):
|
||||
elif self == BoardSizePreset.SMALL:
|
||||
return (min_size + (max_size - min_size) // 4, min_size + (max_size - min_size) // 4)
|
||||
elif self == BoardSizePreset.MEDIUM:
|
||||
return (max_size // 2, max_size // 2)
|
||||
return (min_size + (max_size - min_size) // 2, min_size + (max_size - min_size) // 2)
|
||||
elif self == BoardSizePreset.LARGE:
|
||||
return (min_size + 3 * (max_size - min_size) // 4, min_size + 3 * (max_size - min_size) // 4)
|
||||
elif self == BoardSizePreset.VERY_LARGE:
|
||||
|
||||
@ -109,12 +109,30 @@ class MultiplayerSessionManager(object):
|
||||
self._sessions_lock = asyncio.Lock()
|
||||
self._grid_block_ratio = ServerConfig.get_config().GRID_BLOCK_RATIO
|
||||
|
||||
async def remove_idle_sessions(self):
|
||||
"""Remove all idle sessions that have exceeded their max idle time."""
|
||||
async with self._sessions_lock:
|
||||
idle_session_ids = [
|
||||
session_id for session_id, session in self._sessions.items()
|
||||
if session.is_idle
|
||||
]
|
||||
for session_id in idle_session_ids:
|
||||
logger.info("Removing idle session %s", session_id)
|
||||
del self._sessions[session_id]
|
||||
if idle_session_ids:
|
||||
logger.info("Removed %d idle sessions", len(idle_session_ids))
|
||||
|
||||
async def create_session(self, lang: str | Languages, grid_w: int, grid_h: int) -> MultiplayerSession:
|
||||
# Remove idle sessions before creating a new one
|
||||
await self.remove_idle_sessions()
|
||||
async with self._sessions_lock:
|
||||
|
||||
|
||||
if isinstance(lang, str):
|
||||
lang = Languages(lang)
|
||||
dictionary = lang.load_dictionary()
|
||||
max_tries = 4
|
||||
for i in range(max_tries):
|
||||
crossword = Crossword.generate(
|
||||
dictionary=dictionary,
|
||||
seed=None,
|
||||
@ -122,6 +140,8 @@ class MultiplayerSessionManager(object):
|
||||
grid_height=grid_h,
|
||||
grid_block_ratio=self._grid_block_ratio,
|
||||
)
|
||||
if crossword is not None:
|
||||
break
|
||||
if crossword is None:
|
||||
raise RuntimeError("Failed to generate crossword for the given parameters.")
|
||||
session_id = str(uuid.uuid4())
|
||||
@ -146,7 +166,7 @@ class WebsocketCrosswordServer(object):
|
||||
async def handle_request_available_session_properties(handler: WebsocketConnectionHandler, message: client_messages.RequestAvailableSessionPropertiesClientMessage):
|
||||
server_config = ServerConfig.get_config()
|
||||
response = server_messages.AvailableSessionPropertiesServerMessage(
|
||||
supported_languages=[lang.value for lang in Languages],
|
||||
supported_languages=list(reversed([lang.value for lang in Languages])),
|
||||
min_grid_size=server_config.MIN_GRID_SIZE,
|
||||
max_grid_size=server_config.MAX_GRID_SIZE,
|
||||
board_size_presets={
|
||||
@ -227,7 +247,9 @@ class WebsocketCrosswordServer(object):
|
||||
solution_word_positions = []
|
||||
positions = session.crossword.solution_word_positions
|
||||
for pos in positions:
|
||||
solution_word_positions.append((pos[1], pos[0])) # convert (row, col) to (col, row)
|
||||
# Convert from (row, col) to (col, row) for client
|
||||
row, col = pos
|
||||
solution_word_positions.append((col, row))
|
||||
|
||||
response = server_messages.SendFullSessionStateServerMessage(
|
||||
session_id=session.session_id,
|
||||
@ -272,12 +294,29 @@ class WebsocketCrosswordServer(object):
|
||||
if current_grid_letter.upper() == msg_letter.upper():
|
||||
# No change
|
||||
return
|
||||
# check if the letter already is solved, if so, ignore the update
|
||||
words_at_position = crossword.get_words_by_y_x_position(y=message.row, x=message.col)
|
||||
if any(cw.solved for cw in words_at_position):
|
||||
logger.info("Ignoring update to already solved position (%d, %d) in session %s", message.col, message.row, session.session_id)
|
||||
|
||||
# send letter again to client to ensure they have the correct letter
|
||||
msg = server_messages.LetterUpdateBroadcastServerMessage(
|
||||
session_id=session.session_id,
|
||||
row=message.row,
|
||||
col=message.col,
|
||||
letter=msg_letter.upper(),
|
||||
is_solved=is_solved
|
||||
)
|
||||
messages = [msg]
|
||||
|
||||
else:
|
||||
# also check if the position is
|
||||
crossword.place_letter(
|
||||
x=message.col,
|
||||
y=message.row,
|
||||
letter=msg_letter.lower(),
|
||||
)
|
||||
# now check if the word is solved
|
||||
|
||||
words_at_position = crossword.get_words_by_y_x_position(y=message.row, x=message.col)
|
||||
is_solved = any(cw.solved for cw in words_at_position)
|
||||
if is_solved:
|
||||
@ -321,8 +360,6 @@ class WebsocketCrosswordServer(object):
|
||||
await session.send_message_to_all_clients(message=broadcast_message.model_dump())
|
||||
|
||||
|
||||
|
||||
|
||||
def __init__(self, host: str, port: int):
|
||||
self._host = host
|
||||
self._port = port
|
||||
|
||||
@ -21,7 +21,48 @@ def load_dictionary(p: str | Path) -> Dictionary:
|
||||
if not word.isalpha():
|
||||
continue
|
||||
word = word.lower()
|
||||
dict_obj.add_word(Word(word=word, hints=[], difficulty=1))
|
||||
|
||||
hints = []
|
||||
try:
|
||||
if "senses" in obj:
|
||||
for sense in obj["senses"]:
|
||||
text = sense
|
||||
if word in text.lower():
|
||||
continue
|
||||
hints.append(text)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if "synonyms" in obj:
|
||||
for synonym in obj["synonyms"]:
|
||||
text = synonym
|
||||
if word in text.lower():
|
||||
continue
|
||||
hints.append( text)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if "antonyms" in obj:
|
||||
for antonym in obj["antonyms"]:
|
||||
text = antonym
|
||||
if word in text.lower():
|
||||
continue
|
||||
if "de" in p.name:
|
||||
text = "Gegenteil von " + text
|
||||
else:
|
||||
text = "Opposite of " + text
|
||||
hints.append( text)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(hints) > 0:
|
||||
|
||||
w = Word(word=word,
|
||||
hints=hints,
|
||||
difficulty=1)
|
||||
dict_obj.add_word(w)
|
||||
load_dictionary._cache[cache_key] = dict_obj
|
||||
return dict_obj
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 68 KiB |
@ -335,7 +335,7 @@ export class ClueArea extends LitElement {
|
||||
// Show across clues
|
||||
if (this._showAllCluesAcross) {
|
||||
return html`
|
||||
<div class="clue-area">
|
||||
<div class="clue-area expanded">
|
||||
<div class="clue-header">
|
||||
<h3>Across Clues</h3>
|
||||
<button class="clue-toggle" @click="${this._toggleShowAllCluesAcross}">
|
||||
@ -360,7 +360,7 @@ export class ClueArea extends LitElement {
|
||||
// Show down clues
|
||||
if (this._showAllCluesDown) {
|
||||
return html`
|
||||
<div class="clue-area">
|
||||
<div class="clue-area expanded">
|
||||
<div class="clue-header">
|
||||
<h3>Down Clues</h3>
|
||||
<button class="clue-toggle" @click="${this._toggleShowAllCluesDown}">
|
||||
@ -388,7 +388,7 @@ export class ClueArea extends LitElement {
|
||||
<div class="clue-header">
|
||||
${currentClue ? html`
|
||||
<div class="current-clue">
|
||||
<span class="clue-number">${currentClue.number}. ${currentClue.direction}</span>
|
||||
<span class="clue-number">${currentClue.direction === 'across' ? '▶' : '▼'} ${currentClue.number}</span>
|
||||
</div>
|
||||
<div class="clue-text">${currentClue.text}</div>
|
||||
` : html`
|
||||
@ -396,7 +396,7 @@ export class ClueArea extends LitElement {
|
||||
`}
|
||||
|
||||
<div class="clue-toggle-group">
|
||||
<div class="clue-text empty">Show all clues:</div>
|
||||
<div class="clue-text empty">Clues:</div>
|
||||
|
||||
<button class="clue-toggle" @click="${this._toggleShowAllCluesAcross}" title="Show all across clues">
|
||||
<span class="chevron">▶</span>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 12 KiB |
@ -81,20 +81,47 @@ export class CrosswordGrid extends LitElement {
|
||||
this._ensureGrid();
|
||||
// set CSS variables for cell-size and column count; layout done in external stylesheet
|
||||
return html`
|
||||
<div class="main-grid-scroll-container">
|
||||
<div class="grid-container ${this._isSolutionWordComplete() ? 'complete' : ''}">
|
||||
<div class="grid" style="--cell-size: ${this._cellSize}px; --cols: ${this.cols};">
|
||||
${this._grid.map((row, r) => row.map((cell, c) => this._renderCell(r, c, cell))).flat()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this._solutionWordPositions.length > 0 ? html`
|
||||
<h3 style="margin-top: 2rem;">Solution Word</h3>
|
||||
<div class="solution-scroll-container">
|
||||
<h2 style="text-align: center;">Solution Word</h2>
|
||||
<div class="grid solution-word-grid" style="--cell-size: 40px; --cols: ${this._solutionWordPositions.length};">
|
||||
${this._solutionWordPositions.map((pos, i) => this._renderSolutionCell(i, pos))}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
// Set pulse animation delays when grid becomes complete
|
||||
if (this._isSolutionWordComplete()) {
|
||||
this._setPulseDelays();
|
||||
}
|
||||
}
|
||||
|
||||
_setPulseDelays() {
|
||||
const gridContainer = this.querySelector('.grid-container');
|
||||
if (gridContainer && gridContainer.classList.contains('complete')) {
|
||||
const cells = gridContainer.querySelectorAll('.cell');
|
||||
cells.forEach((cell, index) => {
|
||||
// Calculate row and column from index
|
||||
const row = Math.floor(index / this.cols);
|
||||
const col = index % this.cols;
|
||||
// Diagonal wave: delay based on row + col (top-left to bottom-right)
|
||||
const diagonalIndex = row + col;
|
||||
cell.style.setProperty('--pulse-delay', `${diagonalIndex * 0.1}s`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_renderSolutionCell(index, position) {
|
||||
const letter = this._solutionWordValues.get(index) || '';
|
||||
const isSolved = this._solutionWordSolved.has(index);
|
||||
@ -525,6 +552,36 @@ export class CrosswordGrid extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate completion ratio as percentage (0-100)
|
||||
*/
|
||||
_calculateCompletionRatio() {
|
||||
let totalNonWallCells = 0;
|
||||
let solvedCells = 0;
|
||||
|
||||
for (let r = 0; r < this.rows; r++) {
|
||||
for (let c = 0; c < this.cols; c++) {
|
||||
if (this._grid[r][c] !== '#') {
|
||||
totalNonWallCells++;
|
||||
const cellKey = `${r},${c}`;
|
||||
if (this._solvedCells.has(cellKey)) {
|
||||
solvedCells++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalNonWallCells === 0) return 0;
|
||||
return Math.round((solvedCells / totalNonWallCells) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current completion ratio (public method)
|
||||
*/
|
||||
getCompletionRatio() {
|
||||
return this._calculateCompletionRatio();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle letter updates from server (broadcast messages from other players)
|
||||
*/
|
||||
@ -562,6 +619,14 @@ export class CrosswordGrid extends LitElement {
|
||||
|
||||
this.requestUpdate();
|
||||
|
||||
// Calculate and emit completion ratio update
|
||||
const completionRatio = this._calculateCompletionRatio();
|
||||
this.dispatchEvent(new CustomEvent('completion-ratio-changed', {
|
||||
detail: { completionRatio },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
|
||||
// Trigger animation if solution word just completed
|
||||
if (this._isSolutionWordComplete()) {
|
||||
this.updateComplete.then(() => {
|
||||
|
||||
@ -50,6 +50,7 @@
|
||||
let currentSessionId = null;
|
||||
let clueArea = null;
|
||||
let gridElement = null;
|
||||
let isClosingGame = false; // Flag to prevent popstate from reloading session
|
||||
|
||||
// Test notifications
|
||||
notificationManager.success('App loaded successfully');
|
||||
@ -64,7 +65,7 @@
|
||||
function updateUrlWithSessionId(sessionId) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('session_id', sessionId);
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`);
|
||||
window.history.pushState({ sessionId }, '', `${window.location.pathname}?${params.toString()}`);
|
||||
}
|
||||
|
||||
// Helper function to subscribe to a session
|
||||
@ -72,6 +73,15 @@
|
||||
console.log('Subscribing to session:', sessionId);
|
||||
currentSessionId = sessionId;
|
||||
|
||||
// Update URL with session ID
|
||||
updateUrlWithSessionId(sessionId);
|
||||
|
||||
// Show game UI immediately
|
||||
menu.style.display = 'none';
|
||||
gridContainer.style.display = 'block';
|
||||
keyboard.style.display = 'block';
|
||||
gridContainer.innerHTML = '<div class="loading-spinner">Loading session...</div>';
|
||||
|
||||
const message = {
|
||||
type: 'subscribe_session',
|
||||
session_id: sessionId
|
||||
@ -81,6 +91,9 @@
|
||||
notificationManager.info('Loading session...');
|
||||
}
|
||||
|
||||
// Make subscribeToSession available globally for the menu component
|
||||
window.subscribeToSession = subscribeToSession;
|
||||
|
||||
// Handle session creation response
|
||||
wsManager.onMessage('session_created', (message) => {
|
||||
console.log('Session created:', message);
|
||||
@ -124,9 +137,10 @@
|
||||
// Create container with close button
|
||||
gridContainer.innerHTML = `
|
||||
<div class="game-header">
|
||||
<h2>Crossword</h2>
|
||||
<h2 id="crossword-title" style="text-align: center;">Crossword (0%)</h2>
|
||||
<div class="header-buttons">
|
||||
<button class="share-game-btn" aria-label="Share game">
|
||||
<span style="padding-right: 0.5rem;">Share Session</span>
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.15c.52.47 1.2.77 1.96.77 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.44 9.31 6.77 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.77 0 1.44-.3 1.96-.77l7.12 4.16c-.057.21-.087.43-.087.66 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/>
|
||||
</svg>
|
||||
@ -274,6 +288,36 @@
|
||||
clueArea.requestUpdate();
|
||||
});
|
||||
|
||||
// Listen for completion ratio updates
|
||||
gridElement.addEventListener('completion-ratio-changed', (e) => {
|
||||
const { completionRatio } = e.detail;
|
||||
updateHeaderTitle(completionRatio);
|
||||
|
||||
// Update session storage with completion ratio
|
||||
if (window.updateSessionCompletionRatio) {
|
||||
window.updateSessionCompletionRatio(currentSessionId, completionRatio);
|
||||
}
|
||||
});
|
||||
|
||||
// Function to update header title with completion percentage
|
||||
function updateHeaderTitle(completionRatio) {
|
||||
const titleElement = document.getElementById('crossword-title');
|
||||
if (titleElement) {
|
||||
titleElement.textContent = `Crossword (${completionRatio}%)`;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate initial completion ratio after grid is fully set up
|
||||
setTimeout(() => {
|
||||
const initialRatio = gridElement.getCompletionRatio();
|
||||
updateHeaderTitle(initialRatio);
|
||||
|
||||
// Update session storage with initial completion ratio
|
||||
if (window.updateSessionCompletionRatio) {
|
||||
window.updateSessionCompletionRatio(currentSessionId, initialRatio);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Close button handler
|
||||
closeBtn.addEventListener('click', closeGame);
|
||||
|
||||
@ -408,54 +452,8 @@
|
||||
function closeGame() {
|
||||
console.log('Closing game');
|
||||
|
||||
// Clear session ID from URL
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
|
||||
// Reset state
|
||||
currentSessionId = null;
|
||||
|
||||
// Destroy clue area - check multiple ways it could be in the DOM
|
||||
if (clueArea) {
|
||||
if (clueArea.parentNode) {
|
||||
clueArea.parentNode.removeChild(clueArea);
|
||||
}
|
||||
clueArea = null;
|
||||
}
|
||||
|
||||
// Also remove any clue-area elements that might exist
|
||||
const allClueAreas = document.querySelectorAll('clue-area');
|
||||
allClueAreas.forEach(elem => {
|
||||
if (elem.parentNode) {
|
||||
elem.parentNode.removeChild(elem);
|
||||
}
|
||||
});
|
||||
|
||||
// Destroy grid element
|
||||
if (gridElement) {
|
||||
gridElement = null;
|
||||
}
|
||||
|
||||
// Hide grid, show menu
|
||||
menu.style.display = 'block';
|
||||
gridContainer.style.display = 'none';
|
||||
keyboard.style.display = 'none';
|
||||
gridContainer.innerHTML = '';
|
||||
|
||||
// Close and reopen WebSocket to interrupt connection
|
||||
wsManager.close();
|
||||
|
||||
// Reconnect WebSocket after a short delay
|
||||
setTimeout(() => {
|
||||
const wsUrl = menu._getWebsocketUrl ? menu._getWebsocketUrl() : (() => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const host = window.location.hostname;
|
||||
const port = 8765;
|
||||
return `${protocol}://${host}:${port}`;
|
||||
})();
|
||||
|
||||
wsManager.connect(wsUrl);
|
||||
notificationManager.info('Returned to menu');
|
||||
}, 100);
|
||||
// Simply reload the page without session ID to return to menu
|
||||
window.location.href = window.location.pathname;
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
@ -495,6 +493,26 @@
|
||||
gridContainer.innerHTML = '<div class="loading-spinner">Reconnecting to session...</div>';
|
||||
}
|
||||
});
|
||||
|
||||
// Handle back button to switch between sessions
|
||||
window.addEventListener('popstate', (event) => {
|
||||
// Skip if we just closed the game intentionally
|
||||
if (isClosingGame) {
|
||||
console.log('Game is being closed, skipping popstate');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Popstate event:', event);
|
||||
const sessionId = getSessionIdFromUrl();
|
||||
|
||||
if (sessionId && currentSessionId !== sessionId) {
|
||||
console.log('Navigating to session:', sessionId);
|
||||
subscribeToSession(sessionId);
|
||||
} else if (!sessionId) {
|
||||
console.log('No session in URL, showing menu');
|
||||
closeGame();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -41,7 +41,12 @@ export class MobileKeyboard extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class="keyboard-container">
|
||||
${html`<div class="handle" @click=${this._toggleCollapse}>${this.collapsed ? '▲' : '▼'}</div>`}
|
||||
${html`<div class="handle" @click=${this._toggleCollapse}>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" style="vertical-align: middle; margin-right: 0.3rem;">
|
||||
<path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm6 7H5v-2h8v2zm0-4h-2v-2h2v2zm3 0h-2v-2h2v2zm3 0h-2v-2h2v2zm0-3h-2V8h2v2z"/>
|
||||
</svg>
|
||||
${this.collapsed ? '▲' : '▼'}
|
||||
</div>`}
|
||||
<div class="keyboard-wrapper">
|
||||
<div class="keyboard">
|
||||
${rows.map((r, idx) => {
|
||||
@ -136,15 +141,38 @@ export class MobileKeyboard extends LitElement {
|
||||
const wide = (window.innerWidth / window.innerHeight) > 1.6;
|
||||
this._wideScreen = wide;
|
||||
this.classList.toggle('wide-screen', wide);
|
||||
|
||||
// collapsed default: expanded on mobile, collapsed on desktop
|
||||
const wasCollapsed = this.collapsed;
|
||||
if (mobile) this.collapsed = false;
|
||||
else this.collapsed = true;
|
||||
}
|
||||
|
||||
_toggleCollapse() {
|
||||
// Set padding immediately when state changes
|
||||
const main = document.querySelector('main');
|
||||
if (main) {
|
||||
if (this.collapsed) {
|
||||
main.style.paddingBottom = 'var(--page-padding)';
|
||||
} else {
|
||||
const computedHeight = getComputedStyle(this).getPropertyValue('--keyboard-height').trim();
|
||||
main.style.paddingBottom = computedHeight;
|
||||
}
|
||||
}
|
||||
} _toggleCollapse() {
|
||||
this.collapsed = !this.collapsed;
|
||||
if (this.collapsed) this.setAttribute('collapsed', '');
|
||||
else this.removeAttribute('collapsed');
|
||||
if (this.collapsed) {
|
||||
this.setAttribute('collapsed', '');
|
||||
// Remove padding when keyboard is collapsed
|
||||
const main = document.querySelector('main');
|
||||
if (main) main.style.paddingBottom = 'var(--page-padding)';
|
||||
} else {
|
||||
this.removeAttribute('collapsed');
|
||||
// Add padding when keyboard is expanded - get actual computed height
|
||||
const main = document.querySelector('main');
|
||||
if (main) {
|
||||
const computedHeight = getComputedStyle(this).getPropertyValue('--keyboard-height').trim();
|
||||
main.style.paddingBottom = computedHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,9 @@ export class CrosswordMenu extends LitElement {
|
||||
_error: { state: true },
|
||||
_sessionProperties: { state: true },
|
||||
_selectedLanguage: { state: true },
|
||||
_selectedBoardSize: { state: true }
|
||||
_selectedBoardSize: { state: true },
|
||||
_saveSessionsEnabled: { state: true },
|
||||
_savedSessions: { state: true }
|
||||
};
|
||||
}
|
||||
|
||||
@ -24,13 +26,25 @@ export class CrosswordMenu extends LitElement {
|
||||
this._sessionProperties = null;
|
||||
this._selectedLanguage = '';
|
||||
this._selectedBoardSize = '';
|
||||
this._saveSessionsEnabled = false;
|
||||
this._savedSessions = [];
|
||||
this._initializeSessionStorage();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Register notification manager with WebSocket
|
||||
wsManager.setNotificationManager(notificationManager);
|
||||
// Listen for session creation/subscription events
|
||||
wsManager.onMessage('session_created', (msg) => this._onSessionCreated(msg));
|
||||
wsManager.onMessage('full_session_state', (msg) => this._onSessionJoined(msg));
|
||||
wsManager.onMessage('error', (msg) => this._onSessionError(msg));
|
||||
this._initializeConnection();
|
||||
|
||||
// Make update function available globally
|
||||
window.updateSessionCompletionRatio = (sessionId, completionRatio) => {
|
||||
this._updateSessionCompletionRatio(sessionId, completionRatio);
|
||||
};
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@ -38,6 +52,9 @@ export class CrosswordMenu extends LitElement {
|
||||
// Remove message handlers
|
||||
wsManager.offMessage('available_session_properties', this._handleSessionProperties);
|
||||
wsManager.offMessage('error', this._handleError);
|
||||
wsManager.offMessage('session_created', this._onSessionCreated);
|
||||
wsManager.offMessage('full_session_state', this._onSessionJoined);
|
||||
wsManager.offMessage('error', this._onSessionError);
|
||||
}
|
||||
|
||||
_initializeConnection() {
|
||||
@ -58,10 +75,24 @@ export class CrosswordMenu extends LitElement {
|
||||
}
|
||||
|
||||
_getWebsocketUrl() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const host = window.location.hostname;
|
||||
const port = 8765;
|
||||
return `${protocol}://${host}:${port}`;
|
||||
|
||||
// Special case for GitHub Pages deployment
|
||||
if (host === 'antielektron.github.io') {
|
||||
return 'wss://the-cake-is-a-lie.net/crosswords_backend/';
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
|
||||
// If host is localhost, use port 8765. Otherwise, use default port (443 for wss, 80 for ws)
|
||||
const isLocalhost = host === 'localhost' || host === '127.0.0.1';
|
||||
const port = isLocalhost ? 8765 : '';
|
||||
const portStr = port ? `:${port}` : '';
|
||||
|
||||
// If host is localhost, use it as is. Otherwise, add crosswords_backend/ to the path
|
||||
const path = isLocalhost ? '' : 'crosswords_backend/';
|
||||
|
||||
return `${protocol}://${host}${portStr}/${path}`;
|
||||
}
|
||||
|
||||
_requestSessionProperties() {
|
||||
@ -90,7 +121,7 @@ export class CrosswordMenu extends LitElement {
|
||||
|
||||
this._loading = false;
|
||||
this._error = null;
|
||||
notificationManager.success('Game options loaded');
|
||||
notificationManager.success('Connected to Crossword server');
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@ -130,6 +161,197 @@ export class CrosswordMenu extends LitElement {
|
||||
notificationManager.info('Creating session...');
|
||||
}
|
||||
|
||||
_toggleDataInfo() {
|
||||
const element = this.querySelector('.data-info-details');
|
||||
if (element) {
|
||||
element.style.display = element.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Session storage management
|
||||
_initializeSessionStorage() {
|
||||
// Check if the save setting is enabled
|
||||
const saveSettingEnabled = this._getCookie('saveSessionsEnabled');
|
||||
if (saveSettingEnabled === 'true') {
|
||||
this._saveSessionsEnabled = true;
|
||||
|
||||
// Load saved sessions if the setting is enabled
|
||||
const savedSessionsData = this._getCookie('savedSessions');
|
||||
if (savedSessionsData) {
|
||||
try {
|
||||
this._savedSessions = JSON.parse(savedSessionsData);
|
||||
|
||||
// Ensure all sessions have a completionRatio field (for backward compatibility)
|
||||
this._savedSessions = this._savedSessions.map(session => ({
|
||||
...session,
|
||||
completionRatio: session.completionRatio || 0
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse saved sessions cookie:', e);
|
||||
this._clearAllCookies();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||
return null;
|
||||
}
|
||||
|
||||
_setCookie(name, value, days = 30) {
|
||||
const expires = new Date();
|
||||
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
|
||||
}
|
||||
|
||||
_deleteCookie(name) {
|
||||
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
|
||||
}
|
||||
|
||||
_clearAllCookies() {
|
||||
this._deleteCookie('savedSessions');
|
||||
this._deleteCookie('saveSessionsEnabled');
|
||||
this._savedSessions = [];
|
||||
this._saveSessionsEnabled = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_clearSessionsOnly() {
|
||||
this._deleteCookie('savedSessions');
|
||||
this._savedSessions = [];
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_toggleSessionSaving() {
|
||||
this._saveSessionsEnabled = !this._saveSessionsEnabled;
|
||||
if (this._saveSessionsEnabled) {
|
||||
// Save the setting preference when enabled
|
||||
this._setCookie('saveSessionsEnabled', 'true');
|
||||
} else {
|
||||
// Clear everything when disabled
|
||||
this._clearAllCookies();
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_saveSession(sessionId, sessionInfo = {}) {
|
||||
if (!this._saveSessionsEnabled) return;
|
||||
|
||||
// Remove existing entry for this session
|
||||
this._savedSessions = this._savedSessions.filter(s => s.id !== sessionId);
|
||||
|
||||
// Add new entry
|
||||
this._savedSessions.unshift({
|
||||
id: sessionId,
|
||||
timestamp: Date.now(),
|
||||
completionRatio: 0, // Default completion ratio
|
||||
...sessionInfo
|
||||
});
|
||||
|
||||
// Keep only last 10 sessions
|
||||
this._savedSessions = this._savedSessions.slice(0, 10);
|
||||
|
||||
// Save to cookie
|
||||
this._setCookie('savedSessions', JSON.stringify(this._savedSessions));
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_updateSessionCompletionRatio(sessionId, completionRatio) {
|
||||
if (!this._saveSessionsEnabled) return;
|
||||
|
||||
// Find and update the session
|
||||
const sessionIndex = this._savedSessions.findIndex(s => s.id === sessionId);
|
||||
if (sessionIndex !== -1) {
|
||||
this._savedSessions[sessionIndex].completionRatio = completionRatio;
|
||||
this._savedSessions[sessionIndex].timestamp = Date.now(); // Update timestamp
|
||||
|
||||
// Save updated sessions to cookie
|
||||
this._setCookie('savedSessions', JSON.stringify(this._savedSessions));
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
_removeSession(sessionId) {
|
||||
this._savedSessions = this._savedSessions.filter(s => s.id !== sessionId);
|
||||
if (this._savedSessions.length === 0) {
|
||||
this._clearSessionsOnly();
|
||||
} else {
|
||||
this._setCookie('savedSessions', JSON.stringify(this._savedSessions));
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_onSessionCreated(message) {
|
||||
if (message.session_id) {
|
||||
this._saveSession(message.session_id, {
|
||||
type: 'created',
|
||||
language: this._selectedLanguage,
|
||||
boardSize: this._selectedBoardSize
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onSessionJoined(message) {
|
||||
if (message.session_id) {
|
||||
this._saveSession(message.session_id, {
|
||||
type: 'joined'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onSessionError(message) {
|
||||
// Check if it's a session not found error
|
||||
if (message.error_message && message.error_message.includes('session') && message.error_message.includes('not found')) {
|
||||
// Try to extract session ID from error message or use current session ID
|
||||
// This is a fallback - we might not always have the exact session ID in error messages
|
||||
const sessionIdMatch = message.error_message.match(/session\s+([a-f0-9-]+)/i);
|
||||
if (sessionIdMatch) {
|
||||
const sessionId = sessionIdMatch[1];
|
||||
this._removeSession(sessionId);
|
||||
notificationManager.warning(`Session ${sessionId.substring(0, 8)}... no longer exists and was removed from saved sessions`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_reconnectToSession(sessionId) {
|
||||
// Update the timestamp to move this session to the top
|
||||
this._saveSession(sessionId, { type: 'rejoined' });
|
||||
|
||||
// Use the global subscribeToSession function to properly set currentSessionId
|
||||
if (window.subscribeToSession) {
|
||||
window.subscribeToSession(sessionId);
|
||||
} else {
|
||||
// Fallback if function not available
|
||||
const message = {
|
||||
type: 'subscribe_session',
|
||||
session_id: sessionId
|
||||
};
|
||||
wsManager.send(message);
|
||||
notificationManager.info('Reconnecting to session...');
|
||||
}
|
||||
}
|
||||
|
||||
_clearSavedSessions() {
|
||||
this._clearSessionsOnly();
|
||||
notificationManager.info('All saved sessions cleared');
|
||||
}
|
||||
|
||||
_formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffHours = Math.floor((now - date) / (1000 * 60 * 60));
|
||||
const diffMinutes = Math.floor((now - date) / (1000 * 60));
|
||||
if (diffMinutes < 1) return 'Just now';
|
||||
if (diffMinutes < 60) return `${diffMinutes}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this._loading) {
|
||||
return html`
|
||||
@ -152,7 +374,9 @@ export class CrosswordMenu extends LitElement {
|
||||
return html`
|
||||
<div class="menu-container">
|
||||
<div class="menu">
|
||||
<h1>Crossword</h1>
|
||||
<h1>Multiplayer Crossword</h1>
|
||||
|
||||
<p class="description">Collaborate with others to solve crosswords in real-time. Create a session and share the link with friends to play together!</p>
|
||||
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ''}
|
||||
|
||||
@ -174,7 +398,53 @@ export class CrosswordMenu extends LitElement {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" ?checked="${this._saveSessionsEnabled}" @change="${this._toggleSessionSaving}">
|
||||
Save list of recently joined sessions (uses cookies)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button @click="${this._onCreateCrossword}">Create Crossword</button>
|
||||
|
||||
${this._savedSessions.length > 0 ? html`
|
||||
<div class="saved-sessions">
|
||||
<h3>Recent Sessions</h3>
|
||||
<div class="session-list">
|
||||
${this._savedSessions.map(session => html`
|
||||
<div class="session-item">
|
||||
<div class="session-info">
|
||||
<span class="session-id">${session.id.substring(0, 8)}...</span>
|
||||
<span class="session-time">${this._formatTimestamp(session.timestamp)}</span>
|
||||
${session.language ? html`<span class="session-lang">${session.language.toUpperCase()}</span>` : ''}
|
||||
<span class="session-completion">${session.completionRatio || 0}% solved</span>
|
||||
</div>
|
||||
<div class="session-actions">
|
||||
<button class="reconnect-btn" @click="${() => this._reconnectToSession(session.id)}">Rejoin</button>
|
||||
<button class="remove-btn" @click="${() => this._removeSession(session.id)}">×</button>
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
<button class="clear-all-btn" @click="${this._clearSavedSessions}">Clear All Sessions</button>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="data-info">
|
||||
<span class="data-info-toggle" @click="${this._toggleDataInfo}">▶ 📋 Data Usage for the Multiplayer functionality</span>
|
||||
<div class="data-info-details" style="display: none;">
|
||||
<ul>
|
||||
<li><strong>Shared Data:</strong> Only the letters you type into the grid during a session are shared with other users and the backend server in that session.</li>
|
||||
<li><strong>Session Lifetime:</strong> Sessions are automatically deleted after 48 hours of inactivity.</li>
|
||||
<li><strong>No Tracking:</strong> No personal data is collected or stored beyond the session duration.</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: 12px; font-size: 0.9em;">
|
||||
|
||||
<a href="https://the-cake-is-a-lie.net/gitea/jonas/multiplayer_crosswords" target="_blank" rel="noopener noreferrer">🔗 View source code on Gitea</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -1,8 +1,25 @@
|
||||
:root {
|
||||
--keyboard-space: 30vh; /* reserve ~30% of viewport height for keyboard on small screens */
|
||||
--page-padding: clamp(0.75rem, 3.5vw, 2rem);
|
||||
--keyboard-space: 30vh; /* fallback - will be calculated dynamically */
|
||||
--page-padding: 0rem;
|
||||
--max-container-width: 720px;
|
||||
|
||||
/* Calculated keyboard height - all values proportional to key-width for consistency */
|
||||
--key-width: clamp(1.7rem, 5.5vw, 2.1rem);
|
||||
--key-height: calc(var(--key-width) * 1.4);
|
||||
--keyboard-gap: calc(var(--key-width) * 0.2); /* gap between rows, proportional to key size */
|
||||
--keyboard-padding: calc(var(--key-width) * 0.4); /* keyboard padding, proportional to key size */
|
||||
--handle-height: calc(var(--key-width) * 0.8); /* handle height, proportional to key size */
|
||||
--handle-padding: calc(var(--key-width) * 0.2); /* handle padding, proportional to key size */
|
||||
--keyboard-border: 2px; /* fixed border */
|
||||
--keyboard-buffer: calc(var(--key-width) * 0.3); /* bottom buffer, proportional to key size */
|
||||
|
||||
--keyboard-height: calc(
|
||||
(var(--key-height) * 4) + /* 4 rows of keys */
|
||||
(var(--keyboard-gap) * 3) + /* 3 gaps between rows */
|
||||
(var(--keyboard-padding) * 2) + /* keyboard padding top/bottom */
|
||||
var(--keyboard-border) + /* keyboard border-top */
|
||||
);
|
||||
|
||||
/* Crossword color palette - lighter, more paper-like */
|
||||
--paper-bg: #f9f7f3;
|
||||
--paper-light: #fcfaf8;
|
||||
@ -42,15 +59,14 @@ main {
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: var(--page-padding);
|
||||
padding-bottom: 0; /* Remove keyboard space by default (desktop) */
|
||||
min-height: calc(100vh - 0px);
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Add keyboard space on mobile devices */
|
||||
/* On mobile devices, expand main to fill more space when keyboard is collapsed */
|
||||
@media (max-width: 900px) {
|
||||
main {
|
||||
padding-bottom: var(--keyboard-space);
|
||||
min-height: calc(100vh - var(--keyboard-space));
|
||||
}
|
||||
}
|
||||
.container {
|
||||
@ -82,7 +98,9 @@ crossword-grid { display: block; margin: 0 auto; }
|
||||
display: inline-block;
|
||||
gap: 0;
|
||||
overflow: visible;
|
||||
padding: 20px;
|
||||
/* padding only top and bottom for better centering */
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
@ -94,10 +112,7 @@ crossword-grid { display: block; margin: 0 auto; }
|
||||
border: none;
|
||||
}
|
||||
|
||||
.grid-container.complete .grid {
|
||||
animation: grid-glow 2s ease-in-out infinite;
|
||||
will-change: box-shadow;
|
||||
}
|
||||
/* Grid glow animation removed - now using solution letter glow */
|
||||
|
||||
.cell {
|
||||
width: var(--cell-size, 4rem);
|
||||
@ -249,6 +264,13 @@ crossword-grid { display: block; margin: 0 auto; }
|
||||
display: block;
|
||||
}
|
||||
|
||||
.solution-word-grid {
|
||||
max-width: 100%;
|
||||
width: fit-content; /* Only take the space needed */
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.grid .cell .solution-circle {
|
||||
display: block;
|
||||
}
|
||||
@ -293,15 +315,6 @@ crossword-grid { display: block; margin: 0 auto; }
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes grid-glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 20px rgba(100, 200, 100, 0.3), inset 0 0 20px rgba(100, 200, 100, 0.1);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 40px rgba(100, 200, 100, 0.6), inset 0 0 40px rgba(100, 200, 100, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.cell.solved {
|
||||
background:
|
||||
repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(100,200,100,.1) 3px, rgba(100,200,100,.1) 5px),
|
||||
@ -325,6 +338,32 @@ crossword-grid { display: block; margin: 0 auto; }
|
||||
inset 1px 1px 2px rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
/* Solved cells that are also highlighted - keep green background but yellow border */
|
||||
.cell.solved.mode-highlighted {
|
||||
background:
|
||||
repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(100,200,100,.1) 3px, rgba(100,200,100,.1) 5px),
|
||||
repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(100,200,100,.08) 4px, rgba(100,200,100,.08) 6px),
|
||||
repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(100,200,100,.06) 3px, rgba(100,200,100,.06) 5px),
|
||||
repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(100,200,100,.04) 4px, rgba(100,200,100,.04) 6px),
|
||||
repeating-radial-gradient(circle at 12% 18%, rgba(100,200,100,.06) 1px, transparent 1px),
|
||||
repeating-radial-gradient(circle at 67% 77%, rgba(100,200,100,.07) 1px, transparent 1px),
|
||||
repeating-radial-gradient(circle at 34% 51%, rgba(100,200,100,.03) 1.5px, transparent 1.5px),
|
||||
repeating-radial-gradient(circle at 23% 67%, rgba(100,200,100,.015) 0.8px, transparent 0.8px),
|
||||
repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.03) 0.8px, transparent 0.8px),
|
||||
radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.2) 0%, transparent 40%),
|
||||
radial-gradient(circle at 0% 0%, rgba(100,200,100,.015) 0%, transparent 70%),
|
||||
radial-gradient(circle at 100% 100%, rgba(100,200,100,.015) 0%, transparent 70%),
|
||||
linear-gradient(135deg, #d4f4d4 0%, #c8ead4 100%);
|
||||
box-shadow:
|
||||
inset 0 1px 2px rgba(255,255,255,0.8),
|
||||
inset 0 0 0 1px #c8e6f0,
|
||||
0 0.5px 1px rgba(0,0,0,0.05),
|
||||
inset -1px -1px 2px rgba(100,200,100,0.08),
|
||||
inset 1px 1px 2px rgba(255,255,255,0.3);
|
||||
border-color: #a8d4e8;
|
||||
}
|
||||
|
||||
|
||||
@keyframes cell-bounce {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
@ -340,25 +379,9 @@ crossword-grid { display: block; margin: 0 auto; }
|
||||
}
|
||||
}
|
||||
|
||||
.solution-word-grid.complete .cell {
|
||||
animation: cell-bounce 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
.solution-word-grid.complete .cell:nth-child(1) { animation-delay: 0s; }
|
||||
.solution-word-grid.complete .cell:nth-child(2) { animation-delay: 0.1s; }
|
||||
.solution-word-grid.complete .cell:nth-child(3) { animation-delay: 0.2s; }
|
||||
.solution-word-grid.complete .cell:nth-child(4) { animation-delay: 0.3s; }
|
||||
.solution-word-grid.complete .cell:nth-child(5) { animation-delay: 0.4s; }
|
||||
.solution-word-grid.complete .cell:nth-child(6) { animation-delay: 0.5s; }
|
||||
.solution-word-grid.complete .cell:nth-child(7) { animation-delay: 0.6s; }
|
||||
.solution-word-grid.complete .cell:nth-child(8) { animation-delay: 0.7s; }
|
||||
.solution-word-grid.complete .cell:nth-child(9) { animation-delay: 0.8s; }
|
||||
.solution-word-grid.complete .cell:nth-child(10) { animation-delay: 0.9s; }
|
||||
.solution-word-grid.complete .cell:nth-child(11) { animation-delay: 1.0s; }
|
||||
.solution-word-grid.complete .cell:nth-child(12) { animation-delay: 1.1s; }
|
||||
.solution-word-grid.complete .cell:nth-child(13) { animation-delay: 1.2s; }
|
||||
.solution-word-grid.complete .cell:nth-child(14) { animation-delay: 1.3s; }
|
||||
.solution-word-grid.complete .cell:nth-child(15) { animation-delay: 1.4s; }
|
||||
|
||||
|
||||
|
||||
.cell.selected {
|
||||
outline: none;
|
||||
@ -385,6 +408,32 @@ crossword-grid { display: block; margin: 0 auto; }
|
||||
inset 1px 1px 2px rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
/* Solved cells that are also selected - keep green background but yellow border */
|
||||
.cell.solved.selected {
|
||||
outline: none;
|
||||
background:
|
||||
repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(100,200,100,.1) 3px, rgba(100,200,100,.1) 5px),
|
||||
repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(100,200,100,.08) 4px, rgba(100,200,100,.08) 6px),
|
||||
repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(100,200,100,.06) 3px, rgba(100,200,100,.06) 5px),
|
||||
repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(100,200,100,.04) 4px, rgba(100,200,100,.04) 6px),
|
||||
repeating-radial-gradient(circle at 12% 18%, rgba(100,200,100,.06) 1px, transparent 1px),
|
||||
repeating-radial-gradient(circle at 67% 77%, rgba(100,200,100,.07) 1px, transparent 1px),
|
||||
repeating-radial-gradient(circle at 34% 51%, rgba(100,200,100,.03) 1.5px, transparent 1.5px),
|
||||
repeating-radial-gradient(circle at 23% 67%, rgba(100,200,100,.015) 0.8px, transparent 0.8px),
|
||||
repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.03) 0.8px, transparent 0.8px),
|
||||
radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.2) 0%, transparent 40%),
|
||||
radial-gradient(circle at 0% 0%, rgba(100,200,100,.015) 0%, transparent 70%),
|
||||
radial-gradient(circle at 100% 100%, rgba(100,200,100,.015) 0%, transparent 70%),
|
||||
linear-gradient(135deg, #d4f4d4 0%, #c8ead4 100%) !important;
|
||||
border-color: var(--ink-dark) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 2px rgba(255,255,255,0.8),
|
||||
inset 0 0 0 1.5px #ffc107,
|
||||
0 0 8px rgba(255,193,7,0.25),
|
||||
inset -1px -1px 2px rgba(100,200,100,0.08),
|
||||
inset 1px 1px 2px rgba(255,255,255,0.3) !important;
|
||||
}
|
||||
|
||||
.cell.selected.mode-highlighted {
|
||||
background:
|
||||
repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(200,150,0,.15) 3px, rgba(200,150,0,.15) 5px),
|
||||
@ -448,8 +497,8 @@ mobile-keyboard[collapsed] .keyboard-wrapper {
|
||||
mobile-keyboard .keyboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
padding: 0.75em;
|
||||
gap: var(--keyboard-gap);
|
||||
padding: var(--keyboard-padding);
|
||||
background:
|
||||
repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px),
|
||||
repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px),
|
||||
@ -657,26 +706,38 @@ mobile-keyboard .key-spacer {
|
||||
|
||||
mobile-keyboard .handle {
|
||||
position: absolute;
|
||||
top: -2.4rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: calc(-1 * var(--handle-height));
|
||||
left: var(--keyboard-padding);
|
||||
transform: none;
|
||||
background:
|
||||
repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px),
|
||||
repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px),
|
||||
repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px),
|
||||
repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px),
|
||||
repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.02) 1px, transparent 1px),
|
||||
repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.03) 1px, transparent 1px),
|
||||
repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.015) 1.5px, transparent 1.5px),
|
||||
linear-gradient(135deg, #0a0805 0%, #0f0c09 100%);
|
||||
color: #a89f99;
|
||||
padding: 0.55rem 0.9rem;
|
||||
repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.01) 3px, rgba(255,255,255,.01) 5px),
|
||||
repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.008) 4px, rgba(255,255,255,.008) 6px),
|
||||
repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.012) 3px, rgba(0,0,0,.012) 5px),
|
||||
repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.008) 4px, rgba(0,0,0,.008) 6px),
|
||||
repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.01) 1px, transparent 1px),
|
||||
repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.015) 1px, transparent 1px),
|
||||
repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.008) 1.5px, transparent 1.5px),
|
||||
linear-gradient(135deg, #1a1512 0%, #0f0c09 100%);
|
||||
color: #7a7268;
|
||||
padding: var(--handle-padding) calc(var(--handle-padding) * 1.5);
|
||||
border-radius: 0.25rem 0.25rem 0 0;
|
||||
border: 1px solid #2a2520;
|
||||
border: 1px solid rgba(58, 53, 48, 0.4);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 -2px 8px rgba(0,0,0,0.5);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 -1px 3px rgba(0,0,0,0.3);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
mobile-keyboard .handle:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
mobile-keyboard[collapsed] .keyboard-container { transform: translateY(calc(100% - 2.6rem)); }
|
||||
@ -684,6 +745,19 @@ mobile-keyboard:not([collapsed]) .keyboard-container { transform: translateY(0);
|
||||
|
||||
@media (max-width: 980px) and (orientation: portrait) {
|
||||
mobile-keyboard .keyboard { padding-bottom: 1.6rem; }
|
||||
|
||||
/* Override :root variables globally for portrait mode */
|
||||
:root {
|
||||
--keyboard-height: calc(
|
||||
(calc(8vw * 1.4) * 4) + /* 4 rows with 8vw-based key-height */
|
||||
(calc(8vw * 0.2) * 3) + /* 3 gaps between rows (8vw-based) */
|
||||
calc(8vw * 0.4) + /* bottom padding (8vw-based) - the 1.6rem is separate */
|
||||
calc(8vw * 0.8) + /* handle height (8vw-based) */
|
||||
2px + /* keyboard border-top */
|
||||
0.6rem /* visual buffer for shadow effects */
|
||||
);
|
||||
}
|
||||
|
||||
mobile-keyboard {
|
||||
width: 100%;
|
||||
--key-width: 8vw;
|
||||
@ -827,6 +901,113 @@ crossword-menu {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Session management styles */
|
||||
.saved-sessions {
|
||||
margin-top: 2rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.saved-sessions h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #232842;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.session-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.session-id {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
color: #222c55;
|
||||
}
|
||||
|
||||
.session-time, .session-lang, .session-completion {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.session-completion {
|
||||
font-weight: 600;
|
||||
color: #4a7a9e;
|
||||
}
|
||||
|
||||
.session-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.reconnect-btn {
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #81c784;
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.reconnect-btn:hover {
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
border-color: rgba(76, 175, 80, 0.5);
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #ef5350;
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
background: rgba(244, 67, 54, 0.3);
|
||||
border-color: rgba(244, 67, 54, 0.5);
|
||||
}
|
||||
|
||||
.clear-all-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #ffab40;
|
||||
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clear-all-btn:hover {
|
||||
background: rgba(255, 152, 0, 0.3);
|
||||
border-color: rgba(255, 152, 0, 0.5);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -993,20 +1174,29 @@ crossword-menu {
|
||||
/* Game Header with Close Button */
|
||||
.game-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0 0.5rem;
|
||||
margin-bottom: 0.2rem;
|
||||
padding: 0.5rem 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.game-header h2 {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* On very small screens, align title to the left */
|
||||
@media (max-width: 600px) {
|
||||
.game-header {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.share-game-btn {
|
||||
@ -1026,7 +1216,7 @@ crossword-menu {
|
||||
border: 1px solid rgba(100, 180, 255, 0.3);
|
||||
color: #f5f1ed;
|
||||
cursor: pointer;
|
||||
width: 2.5rem;
|
||||
/* width: 2.5rem; */
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -1035,6 +1225,7 @@ crossword-menu {
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255,255,255,0.08);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.share-game-btn:hover {
|
||||
@ -1111,20 +1302,42 @@ crossword-menu {
|
||||
.game-content {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
overflow-x: auto; /* Allow horizontal scrolling */
|
||||
max-height: calc(100vh - 200px); /* Leave space for header, clue area, and keyboard */
|
||||
width: 100%;
|
||||
padding: 1rem 0; /* Add some padding for breathing room */
|
||||
text-align: center; /* Center inline-block children */
|
||||
}
|
||||
|
||||
crossword-grid {
|
||||
display: inline-block;
|
||||
min-width: fit-content; /* Ensure grid takes its full needed width */
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-radius: 0px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Completely independent scroll containers */
|
||||
.main-grid-scroll-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
max-width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main-grid-scroll-container .grid-container {
|
||||
display: inline-block;
|
||||
min-width: fit-content;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.solution-scroll-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
max-width: 100%;
|
||||
display: inline-block; /* Behave like inline-block to only take needed space */
|
||||
width: auto; /* Auto width to fit content */
|
||||
}
|
||||
|
||||
/* Clue Area */
|
||||
.clue-area {
|
||||
position: fixed;
|
||||
@ -1147,8 +1360,14 @@ crossword-grid {
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
max-height: 3.5rem;
|
||||
transition: max-height 0.3s ease;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
/* Expanded clue area - show all clues */
|
||||
.clue-area.expanded {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.clue-header {
|
||||
@ -1162,6 +1381,7 @@ crossword-grid {
|
||||
font-weight: 600;
|
||||
color: #a8b8ff;
|
||||
font-size: 0.875rem;
|
||||
min-width: 2rem;
|
||||
}
|
||||
|
||||
.clue-number {
|
||||
@ -1173,8 +1393,12 @@ crossword-grid {
|
||||
.clue-text {
|
||||
color: #f5f1ed;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
line-height: 1.7;
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: scroll;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.clue-text.empty {
|
||||
@ -1200,8 +1424,8 @@ crossword-grid {
|
||||
color: #f5f1ed;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -1239,6 +1463,7 @@ crossword-grid {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
}
|
||||
|
||||
/* All Clues View */
|
||||
@ -1307,6 +1532,50 @@ crossword-grid {
|
||||
|
||||
/* Adjust main content for clue area */
|
||||
main {
|
||||
padding-top: calc(var(--page-padding) + 6rem);
|
||||
padding-top: calc(var(--page-padding) + 4rem);
|
||||
}
|
||||
|
||||
/* Solution circle rotation animation */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Solution circles spin continuously when complete */
|
||||
@keyframes spin-with-translate {
|
||||
from {
|
||||
transform: translate(calc(-50% + var(--cell-size) * 0.0625), calc(-50% + var(--cell-size) * 0.0625)) rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: translate(calc(-50% + var(--cell-size) * 0.0625), calc(-50% + var(--cell-size) * 0.0625)) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.solution-word-grid.complete .solution-circle,
|
||||
.grid-container.complete .solution-circle {
|
||||
animation: spin-with-translate 3s linear infinite;
|
||||
}
|
||||
|
||||
/* Pulse animation for all grid cells when puzzle is solved */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
.grid-container.complete .cell {
|
||||
animation: pulse 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
/* Staggered animation delays using CSS custom property */
|
||||
.grid-container.complete .cell {
|
||||
animation-delay: var(--pulse-delay, 0s);
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
const cacheName = 'pwa-conf-v4';
|
||||
const staticAssets = [
|
||||
'./app.js ',
|
||||
'./websocket.js' //TODO: add all necessary files
|
||||
|
||||
'./',
|
||||
'./index.html',
|
||||
'./app.js',
|
||||
'./main.js',
|
||||
'./websocket.js',
|
||||
'./grid.js',
|
||||
'./clue_area.js',
|
||||
'./keyboard.js',
|
||||
'./menu.js',
|
||||
'./notification-area.js',
|
||||
'./notification-manager.js',
|
||||
'./styles.css',
|
||||
'./manifest.json',
|
||||
'./favicon.png',
|
||||
'./big_icon.png'
|
||||
];
|
||||
|
||||
|
||||
|
||||
@ -111,16 +111,16 @@ class WebSocketManager {
|
||||
* Internal handler - called on socket close
|
||||
*/
|
||||
_onClose(event) {
|
||||
console.log('WebSocket closed');
|
||||
console.log('WebSocket closed - reloading page');
|
||||
if (this.notificationManager) {
|
||||
this.notificationManager.info('Connection lost, reconnecting...', 0); // No auto-dismiss
|
||||
this.notificationManager.info('Connection lost, reloading...', 2000);
|
||||
}
|
||||
this._callHandlers('close', { type: 'close' });
|
||||
|
||||
if (!this.isReconnecting) {
|
||||
this.isReconnecting = true;
|
||||
setTimeout(() => this.connect(this.url), this.reconnectDelay);
|
||||
}
|
||||
// Simply reload the page instead of trying to reconnect
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "multiplayer-crosswords"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = ""
|
||||
authors = [
|
||||
{name="Jonas Weinz"}
|
||||
@ -17,7 +17,7 @@ dependencies = [
|
||||
]
|
||||
[tool.poetry]
|
||||
name = "multiplayer-crosswords"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = ""
|
||||
authors = [
|
||||
"Jonas Weinz"
|
||||
@ -31,6 +31,17 @@ pytest = "^7.0"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
jupyterlab = "^4.4.3"
|
||||
|
||||
[[tool.poetry.packages]]
|
||||
include = "multiplayer_crosswords"
|
||||
|
||||
[[tool.poetry.include]]
|
||||
path = "multiplayer_crosswords/webui"
|
||||
format = "sdist"
|
||||
|
||||
[[tool.poetry.include]]
|
||||
path = "multiplayer_crosswords/webui"
|
||||
format = "wheel"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
Reference in New Issue
Block a user