mastefan's picture
Update src/app/viewers.py
755e340 verified
# 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