Anki/ts/routes/card-info/Revlog.svelte
Damien Elmes 9f55cf26fc
Switch to SvelteKit (#3077)
* Update to latest Node LTS

* Add sveltekit

* Split tslib into separate @generated and @tslib components

SvelteKit's path aliases don't support multiple locations, so our old
approach of using @tslib to refer to both ts/lib and out/ts/lib will no
longer work. Instead, all generated sources and their includes are
placed in a separate out/ts/generated folder, and imported via @generated
instead. This also allows us to generate .ts files, instead of needing
to output separate .d.ts and .js files.

* Switch package.json to module type

* Avoid usage of baseUrl

Incompatible with SvelteKit

* Move sass into ts; use relative links

SvelteKit's default sass support doesn't allow overriding loadPaths

* jest->vitest, graphs example working with yarn dev

* most pages working in dev mode

* Some fixes after rebasing

* Fix/silence some svelte-check errors

* Get image-occlusion working with Fabric types

* Post-rebase lock changes

* Editor is now checked

* SvelteKit build integrated into ninja

* Use the new SvelteKit entrypoint for pages like congrats/deck options/etc

* Run eslint once for ts/**; fix some tests

* Fix a bunch of issues introduced when rebasing over latest main

* Run eslint fix

* Fix remaining eslint+pylint issues; tests now all pass

* Fix some issues with a clean build

* Latest bufbuild no longer requires @__PURE__ hack

* Add a few missed dependencies

* Add yarn.bat to fix Windows build

* Fix pages failing to show when ANKI_API_PORT not defined

* Fix svelte-check and vitest on Windows

* Set node path in ./yarn

* Move svelte-kit output to ts/.svelte-kit

Sadly, I couldn't figure out a way to store it in out/ if out/ is
a symlink, as it breaks module resolution when SvelteKit is run.

* Allow HMR inside Anki

* Skip SvelteKit build when HMR is defined

* Fix some post-rebase issues

I should have done a normal merge instead.
2024-03-31 09:16:31 +01:00

211 lines
6.1 KiB
Svelte

<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import type { CardStatsResponse_StatsRevlogEntry as RevlogEntry } from "@generated/anki/stats_pb";
import { RevlogEntry_ReviewKind as ReviewKind } from "@generated/anki/stats_pb";
import * as tr2 from "@generated/ftl";
import { timeSpan, Timestamp } from "@tslib/time";
export let revlog: RevlogEntry[];
function reviewKindClass(entry: RevlogEntry): string {
switch (entry.reviewKind) {
case ReviewKind.LEARNING:
return "revlog-learn";
case ReviewKind.REVIEW:
return "revlog-review";
case ReviewKind.RELEARNING:
return "revlog-relearn";
}
return "";
}
function reviewKindLabel(entry: RevlogEntry): string {
switch (entry.reviewKind) {
case ReviewKind.LEARNING:
return tr2.cardStatsReviewLogTypeLearn();
case ReviewKind.REVIEW:
return tr2.cardStatsReviewLogTypeReview();
case ReviewKind.RELEARNING:
return tr2.cardStatsReviewLogTypeRelearn();
case ReviewKind.FILTERED:
return tr2.cardStatsReviewLogTypeFiltered();
case ReviewKind.MANUAL:
return tr2.cardStatsReviewLogTypeManual();
}
}
function ratingClass(entry: RevlogEntry): string {
if (entry.buttonChosen === 1) {
return "revlog-ease1";
}
return "";
}
interface RevlogRow {
date: string;
time: string;
reviewKind: string;
reviewKindClass: string;
rating: number;
ratingClass: string;
interval: string;
ease: string;
takenSecs: string;
}
function revlogRowFromEntry(entry: RevlogEntry): RevlogRow {
const timestamp = new Timestamp(Number(entry.time));
return {
date: timestamp.dateString(),
time: timestamp.timeString(),
reviewKind: reviewKindLabel(entry),
reviewKindClass: reviewKindClass(entry),
rating: entry.buttonChosen,
ratingClass: ratingClass(entry),
interval: timeSpan(entry.interval),
ease: formatEaseOrDifficulty(entry.ease),
takenSecs: timeSpan(entry.takenSecs, true),
};
}
$: revlogRows = revlog.map(revlogRowFromEntry);
function formatEaseOrDifficulty(ease: number): string {
if (ease === 0) {
return "";
}
const asPct = ease / 10.0;
if (asPct <= 110) {
// FSRS
const unshifted = asPct - 10;
return `D:${unshifted.toFixed(0)}%`;
} else {
// SM-2
return `${asPct.toFixed(0)}%`;
}
}
</script>
{#if revlog.length > 0}
<div class="revlog-table">
<div class="column">
<div class="column-head">{tr2.cardStatsReviewLogDate()}</div>
<div class="column-content">
{#each revlogRows as row, _index}
<div>
<b>{row.date}</b>
<span class="hidden-xs">@ {row.time}</span>
</div>
{/each}
</div>
</div>
<div class="column hidden-xs">
<div class="column-head">{tr2.cardStatsReviewLogType()}</div>
<div class="column-content">
{#each revlogRows as row, _index}
<div class={row.reviewKindClass}>
{row.reviewKind}
</div>
{/each}
</div>
</div>
<div class="column">
<div class="column-head">{tr2.cardStatsReviewLogRating()}</div>
<div class="column-content">
{#each revlogRows as row, _index}
<div class={row.ratingClass}>{row.rating}</div>
{/each}
</div>
</div>
<div class="column">
<div class="column-head">{tr2.cardStatsInterval()}</div>
<div class="column-content right">
{#each revlogRows as row, _index}
<div>{row.interval}</div>
{/each}
</div>
</div>
<div class="column hidden-xs">
<div class="column-head">{tr2.cardStatsEase()}</div>
<div class="column-content">
{#each revlogRows as row, _index}
<div>{row.ease}</div>
{/each}
</div>
</div>
<div class="column">
<div class="column-head">{tr2.cardStatsReviewLogTimeTaken()}</div>
<div class="column-content right">
{#each revlogRows as row, _index}
<div>{row.takenSecs}</div>
{/each}
</div>
</div>
</div>
{/if}
<style lang="scss">
.revlog-table {
width: 100%;
max-width: 50em;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
white-space: nowrap;
}
.column {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&:not(:last-child) {
margin-right: 0.5em;
}
}
.column-head {
font-weight: bold;
}
.column-content {
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: center;
&.right {
align-items: flex-end;
}
> div:empty::after {
/* prevent collapsing of empty rows */
content: "\00a0";
}
}
.revlog-learn {
color: var(--state-new);
}
.revlog-review {
color: var(--state-review);
}
.revlog-relearn,
.revlog-ease1 {
color: var(--state-learn);
}
@media only screen and (max-device-width: 480px) and (orientation: portrait) {
.hidden-xs {
display: none;
}
}
</style>