Spaces:
Sleeping
Sleeping
| # src/app/viewers.py | |
| import json | |
| from pathlib import Path | |
| from typing import Dict, List | |
| from .config import get_user_dir | |
| from .flashcards_tools import load_deck | |
| def _build_flipbook_html(deck_name: str, cards: List[Dict]) -> str: | |
| """ | |
| Builds a simple HTML+JS flip-style viewer for a deck of cards. | |
| Front = card['front'], Back = card['back']. | |
| """ | |
| js_cards = json.dumps( | |
| [ | |
| {"front": c.get("front", ""), "back": c.get("back", "")} | |
| for c in cards | |
| ], | |
| ensure_ascii=False, | |
| ) | |
| html = f"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Flashcards โ {deck_name}</title> | |
| <style> | |
| body {{ | |
| background: #111; | |
| color: #eee; | |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| padding: 1.5rem; | |
| }} | |
| .wrapper {{ | |
| max-width: 700px; | |
| margin: 0 auto; | |
| }} | |
| h1 {{ | |
| text-align: center; | |
| margin-bottom: 1rem; | |
| }} | |
| .card-container {{ | |
| perspective: 1000px; | |
| margin-bottom: 1rem; | |
| }} | |
| .card {{ | |
| position: relative; | |
| width: 100%; | |
| height: 250px; | |
| border-radius: 16px; | |
| background: #222; | |
| box-shadow: 0 8px 15px rgba(0,0,0,0.4); | |
| transition: transform 0.6s; | |
| transform-style: preserve-3d; | |
| cursor: pointer; | |
| }} | |
| .card.flipped {{ | |
| transform: rotateY(180deg); | |
| }} | |
| .card-face {{ | |
| position: absolute; | |
| inset: 0; | |
| border-radius: 16px; | |
| backface-visibility: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 1rem; | |
| }} | |
| .card-face.front {{ | |
| background: #333; | |
| }} | |
| .card-face.back {{ | |
| background: #1a73e8; | |
| transform: rotateY(180deg); | |
| }} | |
| .card-text {{ | |
| font-size: 2.2rem; | |
| text-align: center; | |
| word-wrap: break-word; | |
| }} | |
| .controls {{ | |
| display: flex; | |
| justify-content: center; | |
| gap: 0.75rem; | |
| margin-bottom: 0.5rem; | |
| }} | |
| button {{ | |
| background: #333; | |
| color: #eee; | |
| border-radius: 999px; | |
| border: none; | |
| padding: 0.5rem 1rem; | |
| font-size: 0.95rem; | |
| cursor: pointer; | |
| transition: background 0.2s ease, transform 0.1s ease; | |
| }} | |
| button:hover {{ | |
| background: #444; | |
| transform: translateY(-1px); | |
| }} | |
| .meta {{ | |
| text-align: center; | |
| margin-top: 0.25rem; | |
| font-size: 0.9rem; | |
| color: #ccc; | |
| }} | |
| .badge {{ | |
| display: inline-block; | |
| padding: 0.1rem 0.75rem; | |
| border-radius: 999px; | |
| font-size: 0.8rem; | |
| background: #555; | |
| margin-left: 0.5rem; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrapper"> | |
| <h1>{deck_name}</h1> | |
| <div class="card-container"> | |
| <div class="card" id="card"> | |
| <div class="card-face front"> | |
| <div class="card-text" id="cardFront"></div> | |
| </div> | |
| <div class="card-face back"> | |
| <div class="card-text" id="cardBack"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button id="prevBtn">โฎ Prev</button> | |
| <button id="flipBtn">๐ Flip</button> | |
| <button id="nextBtn">Next โญ</button> | |
| <button id="shuffleBtn">๐ Shuffle</button> | |
| </div> | |
| <div class="meta"> | |
| <span id="cardIndex">Card 0 / 0</span> | |
| <span class="badge" id="sideLabel">Front</span> | |
| </div> | |
| </div> | |
| <script> | |
| const cards = {js_cards}; | |
| let currentIndex = 0; | |
| let isFlipped = false; | |
| const cardEl = document.getElementById('card'); | |
| const frontEl = document.getElementById('cardFront'); | |
| const backEl = document.getElementById('cardBack'); | |
| const indexEl = document.getElementById('cardIndex'); | |
| const sideLabelEl = document.getElementById('sideLabel'); | |
| function renderCard() {{ | |
| if (!cards.length) {{ | |
| frontEl.textContent = "(No cards)"; | |
| backEl.textContent = ""; | |
| indexEl.textContent = "Card 0 / 0"; | |
| sideLabelEl.textContent = "Front"; | |
| cardEl.classList.remove('flipped'); | |
| return; | |
| }} | |
| const card = cards[currentIndex]; | |
| frontEl.textContent = card.front || ""; | |
| backEl.textContent = card.back || ""; | |
| indexEl.textContent = "Card " + (currentIndex + 1) + " / " + cards.length; | |
| sideLabelEl.textContent = isFlipped ? "Back" : "Front"; | |
| }} | |
| function flipCard() {{ | |
| if (!cards.length) return; | |
| isFlipped = !isFlipped; | |
| if (isFlipped) {{ | |
| cardEl.classList.add('flipped'); | |
| }} else {{ | |
| cardEl.classList.remove('flipped'); | |
| }} | |
| sideLabelEl.textContent = isFlipped ? "Back" : "Front"; | |
| }} | |
| function nextCard() {{ | |
| if (!cards.length) return; | |
| currentIndex = (currentIndex + 1) % cards.length; | |
| isFlipped = false; | |
| cardEl.classList.remove('flipped'); | |
| renderCard(); | |
| }} | |
| function prevCard() {{ | |
| if (!cards.length) return; | |
| currentIndex = (currentIndex - 1 + cards.length) % cards.length; | |
| isFlipped = false; | |
| cardEl.classList.remove('flipped'); | |
| renderCard(); | |
| }} | |
| function shuffleCards() {{ | |
| for (let i = cards.length - 1; i > 0; i--) {{ | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [cards[i], cards[j]] = [cards[j], cards[i]]; | |
| }} | |
| currentIndex = 0; | |
| isFlipped = false; | |
| cardEl.classList.remove('flipped'); | |
| renderCard(); | |
| }} | |
| cardEl.addEventListener('click', flipCard); | |
| document.getElementById('flipBtn').addEventListener('click', flipCard); | |
| document.getElementById('nextBtn').addEventListener('click', nextCard); | |
| document.getElementById('prevBtn').addEventListener('click', prevCard); | |
| document.getElementById('shuffleBtn').addEventListener('click', shuffleCards); | |
| document.addEventListener('keydown', (e) => {{ | |
| if (!cards.length) return; | |
| if (e.code === "ArrowRight") nextCard(); | |
| else if (e.code === "ArrowLeft") prevCard(); | |
| else if (e.code === "Space") {{ | |
| e.preventDefault(); | |
| flipCard(); | |
| }} | |
| }}); | |
| renderCard(); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return html | |
| def generate_flashcard_viewer_for_user(username: str, deck_path: Path) -> Path: | |
| """ | |
| Generates an HTML flipbook viewer for the given deck in the user's | |
| /viewers directory, and returns the path to the HTML file. | |
| """ | |
| deck = load_deck(deck_path) | |
| deck_name = deck.get("name", deck_path.stem) | |
| cards = deck.get("cards", []) | |
| html_str = _build_flipbook_html(deck_name, cards) | |
| user_dir = get_user_dir(username) | |
| viewer_dir = user_dir / "viewers" | |
| viewer_dir.mkdir(parents=True, exist_ok=True) | |
| safe_name = deck_path.stem | |
| out_path = viewer_dir / f"{safe_name}_viewer.html" | |
| out_path.write_text(html_str, encoding="utf-8") | |
| return out_path | |