Spaces:
Paused
Paused
Savestate: Implement deck details, manual flashcard addition, and free practice mode
Browse files- index.html +1 -1
- src/db/database.js +19 -29
- src/views/learn/learn-view.js +84 -9
index.html
CHANGED
|
@@ -51,7 +51,7 @@
|
|
| 51 |
</head>
|
| 52 |
<body class="dark-theme in-chat-view">
|
| 53 |
|
| 54 |
-
<div id="live-version-badge" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); font-size: 10px; color:
|
| 55 |
|
| 56 |
<!-- SVG Filters for Liquid Glass Refraction -->
|
| 57 |
<svg width="0" height="0" style="position: absolute; pointer-events: none;">
|
|
|
|
| 51 |
</head>
|
| 52 |
<body class="dark-theme in-chat-view">
|
| 53 |
|
| 54 |
+
<div id="live-version-badge" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); font-size: 10px; color: var(--text-secondary); opacity: 0.6; z-index: 99999; pointer-events: none; font-weight: 500; font-family: monospace; letter-spacing: 1px; background: rgba(0,0,0,0.2); padding: 2px 8px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.05);">BUILD 1.1.48</div>
|
| 55 |
|
| 56 |
<!-- SVG Filters for Liquid Glass Refraction -->
|
| 57 |
<svg width="0" height="0" style="position: absolute; pointer-events: none;">
|
src/db/database.js
CHANGED
|
@@ -346,7 +346,6 @@ class Database {
|
|
| 346 |
[id, originalPath, isDir ? 1 : 0, now]
|
| 347 |
);
|
| 348 |
}
|
| 349 |
-
|
| 350 |
removeTrashItem(id) {
|
| 351 |
if (!this.db) return;
|
| 352 |
this.exec("DELETE FROM trash_metadata WHERE id = ?", [id]);
|
|
@@ -355,23 +354,16 @@ class Database {
|
|
| 355 |
|
| 356 |
getDecks() {
|
| 357 |
try {
|
| 358 |
-
const
|
| 359 |
-
if (res.length === 0) return [];
|
| 360 |
-
|
| 361 |
-
const decks = res[0].values.map(row => {
|
| 362 |
-
const deck = {};
|
| 363 |
-
res[0].columns.forEach((col, i) => deck[col] = row[i]);
|
| 364 |
-
return deck;
|
| 365 |
-
});
|
| 366 |
|
| 367 |
// Get due cards count for each deck
|
| 368 |
const now = new Date().toISOString();
|
| 369 |
decks.forEach(deck => {
|
| 370 |
-
const countRes = this.
|
| 371 |
-
deck.due_count = countRes.length > 0 ? countRes[0].
|
| 372 |
|
| 373 |
-
const totalRes = this.
|
| 374 |
-
deck.total_count = totalRes.length > 0 ? totalRes[0].
|
| 375 |
});
|
| 376 |
|
| 377 |
return decks;
|
|
@@ -388,14 +380,12 @@ class Database {
|
|
| 388 |
"INSERT INTO flashcard_decks (id, title, description, color, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
| 389 |
[id, title, description, color, now, now]
|
| 390 |
);
|
| 391 |
-
this.saveToStorage();
|
| 392 |
return { id, title, description, color, created_at: now, updated_at: now, due_count: 0, total_count: 0 };
|
| 393 |
}
|
| 394 |
|
| 395 |
deleteDeck(deckId) {
|
| 396 |
this.exec("DELETE FROM flashcards WHERE deck_id = ?", [deckId]);
|
| 397 |
this.exec("DELETE FROM flashcard_decks WHERE id = ?", [deckId]);
|
| 398 |
-
this.saveToStorage();
|
| 399 |
}
|
| 400 |
|
| 401 |
addFlashcard(deckId, front, back, imageUrl = null) {
|
|
@@ -408,31 +398,32 @@ class Database {
|
|
| 408 |
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
| 409 |
[id, deckId, front, back, imageUrl, now, 2.5, 0, 0, now, now]
|
| 410 |
);
|
| 411 |
-
this.saveToStorage();
|
| 412 |
return id;
|
| 413 |
}
|
| 414 |
|
| 415 |
getDueCards(deckId) {
|
| 416 |
const now = new Date().toISOString();
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
);
|
| 420 |
-
if (res.length === 0) return [];
|
| 421 |
-
|
| 422 |
-
return res[0].values.map(row => {
|
| 423 |
-
const card = {};
|
| 424 |
-
res[0].columns.forEach((col, i) => card[col] = row[i]);
|
| 425 |
-
return card;
|
| 426 |
-
});
|
| 427 |
}
|
| 428 |
|
| 429 |
// SM-2 Algorithm update
|
| 430 |
updateCardProgress(cardId, quality) {
|
| 431 |
// quality: 0 (Again), 1 (Hard), 2 (Good), 3 (Easy)
|
| 432 |
-
const res = this.
|
| 433 |
if (res.length === 0) return;
|
| 434 |
|
| 435 |
-
let
|
| 436 |
|
| 437 |
if (quality === 0) {
|
| 438 |
repetitions = 0;
|
|
@@ -464,10 +455,9 @@ class Database {
|
|
| 464 |
const now = new Date().toISOString();
|
| 465 |
|
| 466 |
this.exec(
|
| 467 |
-
|
| 468 |
[next_review_iso, ease_factor, interval, repetitions, now, cardId]
|
| 469 |
);
|
| 470 |
-
this.saveToStorage();
|
| 471 |
}
|
| 472 |
|
| 473 |
}
|
|
|
|
| 346 |
[id, originalPath, isDir ? 1 : 0, now]
|
| 347 |
);
|
| 348 |
}
|
|
|
|
| 349 |
removeTrashItem(id) {
|
| 350 |
if (!this.db) return;
|
| 351 |
this.exec("DELETE FROM trash_metadata WHERE id = ?", [id]);
|
|
|
|
| 354 |
|
| 355 |
getDecks() {
|
| 356 |
try {
|
| 357 |
+
const decks = this.exec("SELECT * FROM flashcard_decks ORDER BY created_at DESC");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
// Get due cards count for each deck
|
| 360 |
const now = new Date().toISOString();
|
| 361 |
decks.forEach(deck => {
|
| 362 |
+
const countRes = this.exec("SELECT COUNT(*) as count FROM flashcards WHERE deck_id = ? AND next_review <= ?", [deck.id, now]);
|
| 363 |
+
deck.due_count = countRes.length > 0 ? countRes[0].count : 0;
|
| 364 |
|
| 365 |
+
const totalRes = this.exec("SELECT COUNT(*) as count FROM flashcards WHERE deck_id = ?", [deck.id]);
|
| 366 |
+
deck.total_count = totalRes.length > 0 ? totalRes[0].count : 0;
|
| 367 |
});
|
| 368 |
|
| 369 |
return decks;
|
|
|
|
| 380 |
"INSERT INTO flashcard_decks (id, title, description, color, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
| 381 |
[id, title, description, color, now, now]
|
| 382 |
);
|
|
|
|
| 383 |
return { id, title, description, color, created_at: now, updated_at: now, due_count: 0, total_count: 0 };
|
| 384 |
}
|
| 385 |
|
| 386 |
deleteDeck(deckId) {
|
| 387 |
this.exec("DELETE FROM flashcards WHERE deck_id = ?", [deckId]);
|
| 388 |
this.exec("DELETE FROM flashcard_decks WHERE id = ?", [deckId]);
|
|
|
|
| 389 |
}
|
| 390 |
|
| 391 |
addFlashcard(deckId, front, back, imageUrl = null) {
|
|
|
|
| 398 |
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
| 399 |
[id, deckId, front, back, imageUrl, now, 2.5, 0, 0, now, now]
|
| 400 |
);
|
|
|
|
| 401 |
return id;
|
| 402 |
}
|
| 403 |
|
| 404 |
getDueCards(deckId) {
|
| 405 |
const now = new Date().toISOString();
|
| 406 |
+
return this.exec(
|
| 407 |
+
"SELECT * FROM flashcards WHERE deck_id = ? AND next_review <= ? ORDER BY next_review ASC LIMIT 50",
|
| 408 |
+
[deckId, now]
|
| 409 |
+
);
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
getAllCards(deckId) {
|
| 413 |
+
// For free practice, shuffle them or just order randomly
|
| 414 |
+
return this.exec(
|
| 415 |
+
"SELECT * FROM flashcards WHERE deck_id = ? ORDER BY RANDOM() LIMIT 100",
|
| 416 |
+
[deckId]
|
| 417 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
}
|
| 419 |
|
| 420 |
// SM-2 Algorithm update
|
| 421 |
updateCardProgress(cardId, quality) {
|
| 422 |
// quality: 0 (Again), 1 (Hard), 2 (Good), 3 (Easy)
|
| 423 |
+
const res = this.exec("SELECT ease_factor, interval, repetitions FROM flashcards WHERE id = ?", [cardId]);
|
| 424 |
if (res.length === 0) return;
|
| 425 |
|
| 426 |
+
let { ease_factor, interval, repetitions } = res[0];
|
| 427 |
|
| 428 |
if (quality === 0) {
|
| 429 |
repetitions = 0;
|
|
|
|
| 455 |
const now = new Date().toISOString();
|
| 456 |
|
| 457 |
this.exec(
|
| 458 |
+
"UPDATE flashcards SET next_review = ?, ease_factor = ?, interval = ?, repetitions = ?, updated_at = ? WHERE id = ?",
|
| 459 |
[next_review_iso, ease_factor, interval, repetitions, now, cardId]
|
| 460 |
);
|
|
|
|
| 461 |
}
|
| 462 |
|
| 463 |
}
|
src/views/learn/learn-view.js
CHANGED
|
@@ -44,7 +44,7 @@ export class LearnView {
|
|
| 44 |
decksHtml = `
|
| 45 |
<div class="decks-grid">
|
| 46 |
${this.decks.map(deck => `
|
| 47 |
-
<div class="deck-card" onclick="window.learnView.
|
| 48 |
<div class="deck-icon-wrapper" style="background: ${deck.color}20; color: ${deck.color};">
|
| 49 |
${deck.title.charAt(0).toUpperCase()}
|
| 50 |
</div>
|
|
@@ -61,7 +61,7 @@ export class LearnView {
|
|
| 61 |
|
| 62 |
this.container.innerHTML = `
|
| 63 |
<div class="learn-view">
|
| 64 |
-
<div style="display: flex; justify-content: space-between; align-items: center;">
|
| 65 |
<h2 style="margin: 0; font-size: 28px; color: var(--text-primary);">Lernen</h2>
|
| 66 |
<button id="add-deck-btn" class="icon-button" style="background: var(--surface-2); border-radius: 50%; padding: 8px;">
|
| 67 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
@@ -83,16 +83,91 @@ export class LearnView {
|
|
| 83 |
}
|
| 84 |
}
|
| 85 |
|
| 86 |
-
|
| 87 |
this.activeDeckId = deckId;
|
| 88 |
-
this.
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
if (this.dueCards.length === 0) {
|
| 92 |
alert('Für heute hast du alle Karten in diesem Stapel gelernt!');
|
| 93 |
return;
|
| 94 |
}
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
this.renderStudyMode();
|
| 97 |
}
|
| 98 |
|
|
@@ -102,8 +177,8 @@ export class LearnView {
|
|
| 102 |
<div class="learn-view" style="align-items: center; justify-content: center;">
|
| 103 |
<div style="font-size: 60px; margin-bottom: 20px;">🎉</div>
|
| 104 |
<h2 style="margin: 0 0 10px 0;">Stapel abgeschlossen!</h2>
|
| 105 |
-
<p style="color: var(--text-secondary); margin-bottom: 30px;">Du hast alle
|
| 106 |
-
<button onclick="window.learnView.
|
| 107 |
</div>
|
| 108 |
`;
|
| 109 |
return;
|
|
@@ -116,7 +191,7 @@ export class LearnView {
|
|
| 116 |
this.container.innerHTML = `
|
| 117 |
<div class="learn-view study-container">
|
| 118 |
<div style="width: 100%; max-width: 380px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
| 119 |
-
<button onclick="window.learnView.
|
| 120 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
| 121 |
</button>
|
| 122 |
<span style="color: var(--text-secondary); font-weight: 600;">${this.currentCardIndex + 1} / ${this.dueCards.length}</span>
|
|
|
|
| 44 |
decksHtml = `
|
| 45 |
<div class="decks-grid">
|
| 46 |
${this.decks.map(deck => `
|
| 47 |
+
<div class="deck-card" onclick="window.learnView.renderDeckDetails('${deck.id}')">
|
| 48 |
<div class="deck-icon-wrapper" style="background: ${deck.color}20; color: ${deck.color};">
|
| 49 |
${deck.title.charAt(0).toUpperCase()}
|
| 50 |
</div>
|
|
|
|
| 61 |
|
| 62 |
this.container.innerHTML = `
|
| 63 |
<div class="learn-view">
|
| 64 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
| 65 |
<h2 style="margin: 0; font-size: 28px; color: var(--text-primary);">Lernen</h2>
|
| 66 |
<button id="add-deck-btn" class="icon-button" style="background: var(--surface-2); border-radius: 50%; padding: 8px;">
|
| 67 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
|
|
| 83 |
}
|
| 84 |
}
|
| 85 |
|
| 86 |
+
renderDeckDetails(deckId) {
|
| 87 |
this.activeDeckId = deckId;
|
| 88 |
+
const deck = this.decks.find(d => d.id === deckId);
|
| 89 |
+
if (!deck) return this.renderOverview();
|
| 90 |
+
|
| 91 |
+
// Re-fetch counts
|
| 92 |
+
const dueCards = db.getDueCards(deckId);
|
| 93 |
+
const allCards = db.getAllCards(deckId);
|
| 94 |
+
deck.due_count = dueCards.length;
|
| 95 |
+
deck.total_count = allCards.length;
|
| 96 |
+
|
| 97 |
+
this.container.innerHTML = `
|
| 98 |
+
<div class="learn-view">
|
| 99 |
+
<div style="display: flex; align-items: center; margin-bottom: 24px; gap: 12px;">
|
| 100 |
+
<button onclick="window.learnView.renderOverview()" class="icon-button" style="padding: 8px; border-radius: 50%; background: var(--surface-2);">
|
| 101 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
| 102 |
+
</button>
|
| 103 |
+
<h2 style="margin: 0; font-size: 24px;">${deck.title}</h2>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div style="background: var(--surface-1); border-radius: 20px; padding: 24px; text-align: center; margin-bottom: 24px; border: 1px solid var(--border);">
|
| 107 |
+
<div style="font-size: 48px; margin-bottom: 12px;">${deck.title.charAt(0).toUpperCase()}</div>
|
| 108 |
+
<p style="color: var(--text-secondary); margin-bottom: 24px;">${deck.total_count} Karten insgesamt</p>
|
| 109 |
+
|
| 110 |
+
<div style="display: flex; flex-direction: column; gap: 12px;">
|
| 111 |
+
<button onclick="window.learnView.startStudyDue('${deckId}')" style="padding: 16px; background: var(--primary); color: white; border: none; border-radius: 12px; font-weight: bold; font-size: 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: center;">
|
| 112 |
+
<span>Fällige Karten lernen</span>
|
| 113 |
+
${deck.due_count > 0 ? `<span style="background: rgba(255,255,255,0.2); padding: 4px 10px; border-radius: 20px; font-size: 14px;">${deck.due_count}</span>` : `<span style="opacity: 0.7; font-size: 14px;">Fertig</span>`}
|
| 114 |
+
</button>
|
| 115 |
+
<button onclick="window.learnView.startStudyAll('${deckId}')" style="padding: 16px; background: var(--surface-2); color: var(--text-primary); border: 1px solid var(--border); border-radius: 12px; font-weight: bold; font-size: 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: center;">
|
| 116 |
+
<span>Alle Karten frei üben</span>
|
| 117 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
|
| 118 |
+
</button>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
<h3 style="margin-bottom: 16px; font-size: 18px;">Karten verwalten</h3>
|
| 123 |
+
<div style="background: var(--surface-1); border-radius: 20px; padding: 20px; border: 1px solid var(--border);">
|
| 124 |
+
<div style="display: flex; flex-direction: column; gap: 12px;">
|
| 125 |
+
<input type="text" id="new-card-front" placeholder="Vorderseite (Frage)" style="background: var(--background); border: 1px solid var(--border); color: var(--text-primary); padding: 12px; border-radius: 10px; font-size: 16px;">
|
| 126 |
+
<input type="text" id="new-card-back" placeholder="Rückseite (Antwort)" style="background: var(--background); border: 1px solid var(--border); color: var(--text-primary); padding: 12px; border-radius: 10px; font-size: 16px;">
|
| 127 |
+
<button onclick="window.learnView.addCard('${deckId}')" style="padding: 12px; background: var(--primary); color: white; border: none; border-radius: 10px; font-weight: bold; cursor: pointer;">Hinzufügen</button>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
`;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
addCard(deckId) {
|
| 135 |
+
const frontEl = document.getElementById('new-card-front');
|
| 136 |
+
const backEl = document.getElementById('new-card-back');
|
| 137 |
+
const front = frontEl.value.trim();
|
| 138 |
+
const back = backEl.value.trim();
|
| 139 |
+
if (!front || !back) {
|
| 140 |
+
alert('Bitte fülle Vorder- und Rückseite aus.');
|
| 141 |
+
return;
|
| 142 |
+
}
|
| 143 |
+
db.addFlashcard(deckId, front, back);
|
| 144 |
+
|
| 145 |
+
// Clear inputs and show success
|
| 146 |
+
frontEl.value = '';
|
| 147 |
+
backEl.value = '';
|
| 148 |
|
| 149 |
+
this.renderDeckDetails(deckId); // Refresh stats
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
startStudyDue(deckId) {
|
| 153 |
+
this.dueCards = db.getDueCards(deckId);
|
| 154 |
if (this.dueCards.length === 0) {
|
| 155 |
alert('Für heute hast du alle Karten in diesem Stapel gelernt!');
|
| 156 |
return;
|
| 157 |
}
|
| 158 |
+
this.isFreePractice = false;
|
| 159 |
+
this.currentCardIndex = 0;
|
| 160 |
+
this.renderStudyMode();
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
startStudyAll(deckId) {
|
| 164 |
+
this.dueCards = db.getAllCards(deckId);
|
| 165 |
+
if (this.dueCards.length === 0) {
|
| 166 |
+
alert('Dieser Stapel ist noch leer.');
|
| 167 |
+
return;
|
| 168 |
+
}
|
| 169 |
+
this.isFreePractice = true;
|
| 170 |
+
this.currentCardIndex = 0;
|
| 171 |
this.renderStudyMode();
|
| 172 |
}
|
| 173 |
|
|
|
|
| 177 |
<div class="learn-view" style="align-items: center; justify-content: center;">
|
| 178 |
<div style="font-size: 60px; margin-bottom: 20px;">🎉</div>
|
| 179 |
<h2 style="margin: 0 0 10px 0;">Stapel abgeschlossen!</h2>
|
| 180 |
+
<p style="color: var(--text-secondary); margin-bottom: 30px;">Du hast alle anstehenden Karten gelernt.</p>
|
| 181 |
+
<button onclick="window.learnView.renderDeckDetails('${this.activeDeckId}')" style="padding: 12px 24px; background: var(--primary); border-radius: 20px; color: white; border: none; font-weight: 600; cursor: pointer;">Zurück zum Stapel</button>
|
| 182 |
</div>
|
| 183 |
`;
|
| 184 |
return;
|
|
|
|
| 191 |
this.container.innerHTML = `
|
| 192 |
<div class="learn-view study-container">
|
| 193 |
<div style="width: 100%; max-width: 380px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
| 194 |
+
<button onclick="window.learnView.renderDeckDetails('${this.activeDeckId}')" style="background: none; border: none; color: var(--text-secondary); cursor: pointer;">
|
| 195 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
| 196 |
</button>
|
| 197 |
<span style="color: var(--text-secondary); font-weight: 600;">${this.currentCardIndex + 1} / ${this.dueCards.length}</span>
|