Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 69 additions & 5 deletions app.css
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,10 @@ button.row:active, .deck-row:active { background: var(--c); }
.deck-row:active { transform: scale(.98); }
.deck-row .name { flex: 1; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.deck-row .ic:first-child { color: var(--on-variant); }
.counts { display: flex; gap: 8px; font-size: 12px; line-height: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
.counts { display: flex; gap: 8px; font-size: 11px; line-height: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
.counts .n { color: var(--new); } .counts .l { color: var(--learn); } .counts .d { color: var(--due); }
.counts .zero { color: var(--outline); font-weight: 400; }
.count-lbl { font-size: 10px; font-weight: 500; opacity: 0.8; margin-right: 2px; }
.deck-row .ic.done { color: var(--due); }

.banner {
Expand Down Expand Up @@ -264,7 +265,24 @@ button.row:active, .deck-row:active { background: var(--c); }
.dv-counts .n b { color: var(--new); } .dv-counts .l b { color: var(--learn); } .dv-counts .d b { color: var(--due); }
.card-row { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.card-row .q { font-weight: 600; }
.card-row .a { font-size: 13px; color: var(--on-variant); }
.card-row .a {
font-size: 13px;
color: var(--on-variant);
filter: blur(5.5px);
user-select: none;
cursor: pointer;
transition: filter 0.2s, background 0.2s;
background: color-mix(in srgb, var(--on-variant) 12%, transparent);
border-radius: var(--r-sm);
padding: 0 6px;
display: inline-block;
}
.card-row .a.revealed {
filter: none;
background: none;
padding: 0;
user-select: auto;
}
.card-row .meta { font-size: 12px; color: var(--outline); }
.card-row .q, .card-row .a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }

Expand Down Expand Up @@ -300,8 +318,27 @@ button.row:active, .deck-row:active { background: var(--c); }
.face.back .card-text.dim { font-size: 15px; line-height: 20px; color: var(--on-variant); font-weight: 600; }
.face hr { width: 64px; border: 0; border-top: .5px solid var(--outline-variant); }
.hint { text-align: center; color: var(--outline); font-size: 13px; padding-bottom: 10px; }
.face-tools { display: flex; justify-content: space-between; border-top: .5px solid var(--outline-variant);
padding-top: 6px; margin: 0 -4px -6px; color: var(--on-variant); }
.face-tools {
position: absolute;
top: 12px;
right: 12px;
display: flex;
gap: 8px;
color: var(--on-variant);
}
.face-tools .icon-btn {
width: 36px;
height: 36px;
border-radius: var(--r);
background: var(--c-low);
border: .5px solid var(--outline-variant);
display: grid;
place-items: center;
}
.face-tools .icon-btn .ic {
width: 18px;
height: 18px;
}

.rating { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--stack);
padding: 0 var(--m-page) calc(env(safe-area-inset-bottom) + 16px); }
Expand All @@ -322,11 +359,38 @@ button.row:active, .deck-row:active { background: var(--c); }
flex-direction: column; align-items: center; justify-content: center; gap: 12px;
padding: var(--m-page) 32px calc(var(--m-page) + 40px); text-align: center; animation: fade .3s var(--ease); }
.done-panel[hidden] { display: none; }
.done-check { width: 56px; height: 56px; color: var(--due); stroke-width: 1.6; }
.done-check { width: 56px; height: 56px; color: var(--due); stroke-width: 1.6; animation: pop-check 0.5s var(--ease); }
@keyframes pop-check {
0% { transform: scale(0); }
90% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.done-panel h3 { font-size: 24px; line-height: 30px; }
.done-panel p { color: var(--on-variant); }
.done-panel .primary-btn { max-width: 280px; margin-top: 12px; }

.streak-badge {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: color-mix(in srgb, var(--tertiary) 10%, var(--c-lowest));
border: 1px solid var(--tertiary);
border-radius: var(--r-xl);
padding: 10px 24px;
font-size: 16px;
font-weight: 700;
color: var(--tertiary);
margin: 12px 0;
animation: bounce-in 0.6s var(--ease);
}
@keyframes bounce-in {
0% { transform: scale(0.3); opacity: 0; }
50% { transform: scale(1.08); }
75% { transform: scale(0.95); }
100% { transform: scale(1); opacity: 1; }
}

/* ---------- sheet ---------- */
.scrim { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 30; animation: fadein .25s; }
.scrim[hidden] { display: none; }
Expand Down
74 changes: 70 additions & 4 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ function renderHome() {
${icon('folder')}
<span class="name">${esc(d.name)}</span>
${clear && total ? `${icon('check', 'ic done')}` : `<span class="counts">
<span class="${c.n ? 'n' : 'zero'}">${c.n}</span>
<span class="${c.l ? 'l' : 'zero'}">${c.l}</span>
<span class="${c.d ? 'd' : 'zero'}">${c.d}</span></span>`}
<span class="${c.n ? 'n' : 'zero'}"><span class="count-lbl">新規</span>${c.n}</span>
<span class="${c.l ? 'l' : 'zero'}"><span class="count-lbl">学習中</span>${c.l}</span>
<span class="${c.d ? 'd' : 'zero'}"><span class="count-lbl">復習</span>${c.d}</span></span>`}
${icon('chevron', 'ic chev')}
</button></li>`;
}).join('');
Expand Down Expand Up @@ -246,6 +246,19 @@ function grade(g) {
function finishReview() {
const mins = Math.max(1, Math.round((Date.now() - sessionStart) / 60000));
$('#rv-done-sub').textContent = `${answered}枚を復習しました(約${mins}分)`;

const days = new Set(db.log.map(l => dayKey(l.t)));
let streak = 0, cur = startOfDay(Date.now());
if (!days.has(dayKey(cur))) cur -= DAY;
while (days.has(dayKey(cur))) { streak++; cur -= DAY; }

if (streak > 0) {
$('#rv-streak-count').textContent = streak;
$('#rv-done-streak').hidden = false;
} else {
$('#rv-done-streak').hidden = true;
}

$('#rv-done').hidden = false;
$('#rating').classList.add('hidden');
}
Expand Down Expand Up @@ -346,6 +359,12 @@ function go(tab) {
/* ---------------- events ---------------- */
document.addEventListener('click', e => {
const t = e.target;
if (t.classList.contains('a') && t.closest('.card-row')) {
e.stopPropagation();
e.preventDefault();
t.classList.toggle('revealed');
return;
}
const tab = t.closest('[data-goto]'); if (tab) return go(tab.dataset.goto);
const deck = t.closest('[data-deck]'); if (deck) return openDeckView(deck.dataset.deck);
const card = t.closest('[data-card]'); if (card) return cardSheet(db.cards.find(c => c.id === card.dataset.card));
Expand Down Expand Up @@ -376,7 +395,54 @@ $('#set-theme').onchange = e => { db.settings.theme = e.target.value; save(); ap
$('#opt-' + k).onchange = e => { deckOf(openDeck)[k] = e.target.checked; save(); renderDeck(); };
});
$('#btn-import-json').onclick = () => pickFile('json');
$('#btn-import-csv').onclick = () => pickFile('csv');
function showCSVImportModal() {
const bodyHTML = `
<div style="display: flex; flex-direction: column; gap: 12px;">
<p style="font-size: 14px; line-height: 1.5;">取り込むファイルのフォーマットが以下のように構成されていることを確認してください。</p>
<div style="background: var(--c-lowest); border: .5px solid var(--outline-variant); border-radius: var(--r); padding: 12px; font-size: 13px;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: .5px solid var(--outline-variant); font-weight: 600; text-align: left;">
<th style="padding: 4px 8px;">1列目</th>
<th style="padding: 4px 8px;">2列目</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 6px 8px;">表面(問題)</td>
<td style="padding: 6px 8px;">裏面(答え)</td>
</tr>
</tbody>
</table>
</div>
<p style="font-size: 12px; color: var(--on-variant); line-height: 1.4;">
※カンマ区切り(.csv)およびタブ区切り(.tsv/.txt)に対応しています。ファイル名がそのまま新規作成されるデッキの名前になります。
</p>
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 8px;">
<button class="primary-btn" id="btn-modal-select-file">ファイルを選択</button>
<button class="text-btn" id="btn-modal-sample-dl" style="color: var(--primary); min-height: auto; padding: 6px;">サンプルをダウンロード</button>
</div>
</div>
`;

openSheet('CSV / TSV インポート', bodyHTML, null);

$('#btn-modal-select-file').onclick = () => {
closeSheet();
pickFile('csv');
};

$('#btn-modal-sample-dl').onclick = () => {
const csvContent = "front,back\nubiquitous,遍在する、どこにでもある\nephemeral,つかの間の、短命な\n";
const blob = new Blob([new Uint8Array([0xEF, 0xBB, 0xBF]), csvContent], { type: 'text/csv;charset=utf-8;' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = "sample_deck.csv";
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
};
}
$('#btn-import-csv').onclick = showCSVImportModal;
$('#btn-wipe').onclick = wipe;
$('#file-input').onchange = e => e.target.files[0] && handleFile(e.target.files[0]);

Expand Down
10 changes: 5 additions & 5 deletions core.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ function schedule(card, g, now, retention = 0.9) {
}

function fmtInterval(days) {
if (days < 1 / 24) return '<1m';
if (days < 1) return Math.round(days * 24) + 'h';
if (days < 30) return Math.round(days) + 'd';
if (days < 365) return (days / 30.4).toFixed(days < 90 ? 1 : 0) + 'mo';
return (days / 365).toFixed(1) + 'y';
if (days < 1 / 24) return '<1分';
if (days < 1) return Math.round(days * 24) + '時間後';
if (days < 30) return Math.round(days) + '日後';
if (days < 365) return (days / 30.4).toFixed(days < 90 ? 1 : 0) + 'ヶ月後';
return (days / 365).toFixed(1) + '年後';
}

/** Minimal RFC4180 parser; auto-detects TSV (Anki's export format). */
Expand Down
11 changes: 9 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@
<main id="tab-home" class="screen" data-tab="home">
<header class="large-title">
<h1>デッキ</h1>
<button class="icon-btn accent" id="btn-add-deck" aria-label="デッキを追加"><svg class="ic" viewBox="0 0 24 24"><use href="#i-plus"/></svg></button>
<button class="icon-btn accent" id="btn-add-deck" aria-label="デッキを追加" style="width: auto; padding: 0 12px; display: flex; align-items: center; gap: 4px;">
<svg class="ic" viewBox="0 0 24 24"><use href="#i-plus"/></svg>
<span style="font-size: 14px; font-weight: 600;">デッキ追加</span>
</button>
</header>
<div class="scroll">
<div id="backup-banner"></div>
Expand Down Expand Up @@ -186,8 +189,12 @@ <h2 id="rv-deck"></h2>
</div>
<div class="done-panel" id="rv-done" hidden>
<svg class="ic done-check" viewBox="0 0 24 24"><use href="#i-check"/></svg>
<h3>お疲れさまでした</h3>
<h3>お疲れさまでした!</h3>
<p id="rv-done-sub"></p>
<div id="rv-done-streak" class="streak-badge" hidden>
<svg class="ic" viewBox="0 0 24 24" style="color: var(--tertiary); width: 24px; height: 24px;"><use href="#i-flame"/></svg>
<span><strong id="rv-streak-count">0</strong>日連続達成!</span>
</div>
<button class="primary-btn" data-close-review>ホームに戻る</button>
</div>
</section>
Expand Down
11 changes: 0 additions & 11 deletions server.log

This file was deleted.

6 changes: 3 additions & 3 deletions test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ const assert = require('assert');
'lower retention target => longer intervals');

// --- interval labels
assert.strictEqual(fmtInterval(1 / 1440), '<1m');
assert.strictEqual(fmtInterval(4), '4d');
assert.strictEqual(fmtInterval(400), '1.1y');
assert.strictEqual(fmtInterval(1 / 1440), '<1分');
assert.strictEqual(fmtInterval(4), '4日後');
assert.strictEqual(fmtInterval(400), '1.1年後');

// --- CSV / TSV parsing
assert.deepStrictEqual(parseCSV('a,b\n"c,1","d""x"\n\n# note,skip\n,empty').map(r => r.slice(0, 2)),
Expand Down
Loading