mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 00:12:25 -04:00
Merge pull request #1431 from RumovZ/non-modal-card-info
Non modal card info
This commit is contained in:
commit
5eb1208714
11 changed files with 285 additions and 134 deletions
|
@ -22,3 +22,9 @@ card-stats-review-log-type-review = Review
|
||||||
card-stats-review-log-type-relearn = Relearn
|
card-stats-review-log-type-relearn = Relearn
|
||||||
card-stats-review-log-type-filtered = Filtered
|
card-stats-review-log-type-filtered = Filtered
|
||||||
card-stats-review-log-type-manual = Manual
|
card-stats-review-log-type-manual = Manual
|
||||||
|
card-stats-no-card = (No card to display.)
|
||||||
|
|
||||||
|
## Window Titles
|
||||||
|
|
||||||
|
card-stats-current-card = Current Card ({ $context })
|
||||||
|
card-stats-previous-card = Previous Card ({ $context })
|
||||||
|
|
|
@ -28,6 +28,7 @@ def _legacy_card_stats(
|
||||||
) -> str:
|
) -> str:
|
||||||
"A quick hack to preserve compatibility with the old HTML string API."
|
"A quick hack to preserve compatibility with the old HTML string API."
|
||||||
random_id = f"cardinfo-{base62(random.randint(0, 2 ** 64 - 1))}"
|
random_id = f"cardinfo-{base62(random.randint(0, 2 ** 64 - 1))}"
|
||||||
|
varName = random_id.replace("-", "")
|
||||||
return f"""
|
return f"""
|
||||||
<div id="{random_id}"></div>
|
<div id="{random_id}"></div>
|
||||||
<script src="js/vendor/bootstrap.bundle.min.js"></script>
|
<script src="js/vendor/bootstrap.bundle.min.js"></script>
|
||||||
|
@ -38,7 +39,8 @@ def _legacy_card_stats(
|
||||||
if ({1 if _legacy_nightmode else 0}) {{
|
if ({1 if _legacy_nightmode else 0}) {{
|
||||||
document.documentElement.className = "night-mode";
|
document.documentElement.className = "night-mode";
|
||||||
}}
|
}}
|
||||||
anki.cardInfo(document.getElementById('{random_id}'), {card_id}, {include_revlog});
|
const {varName} = anki.cardInfo(document.getElementById('{random_id}'));
|
||||||
|
{varName}.then((c) => c.$set({{ cardId: {card_id}, includeRevlog: {str(include_revlog).lower()} }}));
|
||||||
</script>
|
</script>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ from aqt.utils import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..changenotetype import change_notetype_dialog
|
from ..changenotetype import change_notetype_dialog
|
||||||
from .card_info import CardInfoDialog
|
from .card_info import BrowserCardInfo
|
||||||
from .find_and_replace import FindAndReplaceDialog
|
from .find_and_replace import FindAndReplaceDialog
|
||||||
from .previewer import BrowserPreviewer as PreviewDialog
|
from .previewer import BrowserPreviewer as PreviewDialog
|
||||||
from .previewer import Previewer
|
from .previewer import Previewer
|
||||||
|
@ -110,6 +110,7 @@ class Browser(QMainWindow):
|
||||||
self.lastFilter = ""
|
self.lastFilter = ""
|
||||||
self.focusTo: int | None = None
|
self.focusTo: int | None = None
|
||||||
self._previewer: Previewer | None = None
|
self._previewer: Previewer | None = None
|
||||||
|
self._card_info = BrowserCardInfo(self.mw)
|
||||||
self._closeEventHasCleanedUp = False
|
self._closeEventHasCleanedUp = False
|
||||||
self.form = aqt.forms.browser.Ui_Dialog()
|
self.form = aqt.forms.browser.Ui_Dialog()
|
||||||
self.form.setupUi(self)
|
self.form.setupUi(self)
|
||||||
|
@ -155,6 +156,7 @@ class Browser(QMainWindow):
|
||||||
if changes.browser_table and changes.card:
|
if changes.browser_table and changes.card:
|
||||||
self.card = self.table.get_single_selected_card()
|
self.card = self.table.get_single_selected_card()
|
||||||
self.current_card = self.table.get_current_card()
|
self.current_card = self.table.get_current_card()
|
||||||
|
self._update_card_info()
|
||||||
self._update_current_actions()
|
self._update_current_actions()
|
||||||
|
|
||||||
# changes.card is required for updating flag icon
|
# changes.card is required for updating flag icon
|
||||||
|
@ -236,6 +238,7 @@ class Browser(QMainWindow):
|
||||||
|
|
||||||
def _closeWindow(self) -> None:
|
def _closeWindow(self) -> None:
|
||||||
self._cleanup_preview()
|
self._cleanup_preview()
|
||||||
|
self._card_info.close()
|
||||||
self.editor.cleanup()
|
self.editor.cleanup()
|
||||||
self.table.cleanup()
|
self.table.cleanup()
|
||||||
self.sidebar.cleanup()
|
self.sidebar.cleanup()
|
||||||
|
@ -447,6 +450,7 @@ class Browser(QMainWindow):
|
||||||
return
|
return
|
||||||
self.current_card = self.table.get_current_card()
|
self.current_card = self.table.get_current_card()
|
||||||
self._update_current_actions()
|
self._update_current_actions()
|
||||||
|
self._update_card_info()
|
||||||
|
|
||||||
def _update_row_actions(self) -> None:
|
def _update_row_actions(self) -> None:
|
||||||
has_rows = bool(self.table.len())
|
has_rows = bool(self.table.len())
|
||||||
|
@ -545,10 +549,10 @@ class Browser(QMainWindow):
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def showCardInfo(self) -> None:
|
def showCardInfo(self) -> None:
|
||||||
if not self.current_card:
|
self._card_info.toggle()
|
||||||
return
|
|
||||||
|
|
||||||
CardInfoDialog(parent=self, mw=self.mw, card=self.current_card)
|
def _update_card_info(self) -> None:
|
||||||
|
self._card_info.set_card(self.current_card)
|
||||||
|
|
||||||
# Menu helpers
|
# Menu helpers
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
|
@ -3,8 +3,12 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.cards import Card, CardId
|
from anki.cards import Card, CardId
|
||||||
|
from anki.lang import without_unicode_isolation
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
addCloseShortcut,
|
addCloseShortcut,
|
||||||
|
@ -12,6 +16,8 @@ from aqt.utils import (
|
||||||
qconnect,
|
qconnect,
|
||||||
restoreGeom,
|
restoreGeom,
|
||||||
saveGeom,
|
saveGeom,
|
||||||
|
setWindowIcon,
|
||||||
|
tr,
|
||||||
)
|
)
|
||||||
from aqt.webview import AnkiWebView
|
from aqt.webview import AnkiWebView
|
||||||
|
|
||||||
|
@ -21,18 +27,30 @@ class CardInfoDialog(QDialog):
|
||||||
GEOMETRY_KEY = "revlog"
|
GEOMETRY_KEY = "revlog"
|
||||||
silentlyClose = True
|
silentlyClose = True
|
||||||
|
|
||||||
def __init__(self, parent: QWidget, mw: aqt.AnkiQt, card: Card) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None,
|
||||||
|
mw: aqt.AnkiQt,
|
||||||
|
card: Card | None,
|
||||||
|
on_close: Callable | None = None,
|
||||||
|
geometry_key: str | None = None,
|
||||||
|
window_title: str | None = None,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self._setup_ui(card.id)
|
self._on_close = on_close
|
||||||
|
self.GEOMETRY_KEY = geometry_key or self.GEOMETRY_KEY
|
||||||
|
if window_title:
|
||||||
|
self.setWindowTitle(window_title)
|
||||||
|
self._setup_ui(card.id if card else None)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def _setup_ui(self, card_id: CardId) -> None:
|
def _setup_ui(self, card_id: CardId | None) -> None:
|
||||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
|
||||||
self.mw.garbage_collect_on_dialog_finish(self)
|
self.mw.garbage_collect_on_dialog_finish(self)
|
||||||
disable_help_button(self)
|
disable_help_button(self)
|
||||||
restoreGeom(self, self.GEOMETRY_KEY)
|
restoreGeom(self, self.GEOMETRY_KEY)
|
||||||
addCloseShortcut(self)
|
addCloseShortcut(self)
|
||||||
|
setWindowIcon(self)
|
||||||
|
|
||||||
self.web = AnkiWebView(title=self.TITLE)
|
self.web = AnkiWebView(title=self.TITLE)
|
||||||
self.web.setVisible(False)
|
self.web.setVisible(False)
|
||||||
|
@ -47,10 +65,87 @@ class CardInfoDialog(QDialog):
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
self.web.eval(
|
self.web.eval(
|
||||||
f"anki.cardInfo(document.getElementById('main'), {card_id}, true);"
|
"const cardInfo = anki.cardInfo(document.getElementById('main'));"
|
||||||
|
)
|
||||||
|
self.update_card(card_id)
|
||||||
|
|
||||||
|
def update_card(self, card_id: CardId | None) -> None:
|
||||||
|
self.web.eval(
|
||||||
|
f"cardInfo.then((c) => c.$set({{ cardId: {json.dumps(card_id)} }}));"
|
||||||
)
|
)
|
||||||
|
|
||||||
def reject(self) -> None:
|
def reject(self) -> None:
|
||||||
|
if self._on_close:
|
||||||
|
self._on_close()
|
||||||
self.web = None
|
self.web = None
|
||||||
saveGeom(self, self.GEOMETRY_KEY)
|
saveGeom(self, self.GEOMETRY_KEY)
|
||||||
return QDialog.reject(self)
|
return QDialog.reject(self)
|
||||||
|
|
||||||
|
|
||||||
|
class CardInfoManager:
|
||||||
|
"""Wrapper class to conveniently toggle, update and close a card info dialog."""
|
||||||
|
|
||||||
|
def __init__(self, mw: aqt.AnkiQt, geometry_key: str, window_title: str):
|
||||||
|
self.mw = mw
|
||||||
|
self.geometry_key = geometry_key
|
||||||
|
self.window_title = window_title
|
||||||
|
self._card: Card | None = None
|
||||||
|
self._dialog: CardInfoDialog | None = None
|
||||||
|
|
||||||
|
def toggle(self) -> None:
|
||||||
|
if self._dialog:
|
||||||
|
self._dialog.reject()
|
||||||
|
else:
|
||||||
|
self._dialog = CardInfoDialog(
|
||||||
|
None,
|
||||||
|
self.mw,
|
||||||
|
self._card,
|
||||||
|
self._on_close,
|
||||||
|
self.geometry_key,
|
||||||
|
self.window_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_card(self, card: Card | None) -> None:
|
||||||
|
self._card = card
|
||||||
|
if self._dialog:
|
||||||
|
self._dialog.update_card(card.id if card else None)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if self._dialog:
|
||||||
|
self.toggle()
|
||||||
|
|
||||||
|
def _on_close(self) -> None:
|
||||||
|
self._dialog = None
|
||||||
|
|
||||||
|
|
||||||
|
class BrowserCardInfo(CardInfoManager):
|
||||||
|
def __init__(self, mw: aqt.AnkiQt):
|
||||||
|
super().__init__(
|
||||||
|
mw,
|
||||||
|
"revlog",
|
||||||
|
without_unicode_isolation(
|
||||||
|
tr.card_stats_current_card(context=tr.qt_misc_browse())
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewerCardInfo(CardInfoManager):
|
||||||
|
def __init__(self, mw: aqt.AnkiQt):
|
||||||
|
super().__init__(
|
||||||
|
mw,
|
||||||
|
"reviewerCardInfo",
|
||||||
|
without_unicode_isolation(
|
||||||
|
tr.card_stats_current_card(context=tr.decks_study())
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PreviousReviewerCardInfo(CardInfoManager):
|
||||||
|
def __init__(self, mw: aqt.AnkiQt):
|
||||||
|
super().__init__(
|
||||||
|
mw,
|
||||||
|
"previousReviewerCardInfo",
|
||||||
|
without_unicode_isolation(
|
||||||
|
tr.card_stats_previous_card(context=tr.decks_study())
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
@ -17,9 +17,7 @@ from aqt.qt import (
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
QIcon,
|
|
||||||
QKeySequence,
|
QKeySequence,
|
||||||
QPixmap,
|
|
||||||
QShortcut,
|
QShortcut,
|
||||||
Qt,
|
Qt,
|
||||||
QTimer,
|
QTimer,
|
||||||
|
@ -30,7 +28,7 @@ from aqt.qt import (
|
||||||
from aqt.reviewer import replay_audio
|
from aqt.reviewer import replay_audio
|
||||||
from aqt.sound import av_player, play_clicked_audio
|
from aqt.sound import av_player, play_clicked_audio
|
||||||
from aqt.theme import theme_manager
|
from aqt.theme import theme_manager
|
||||||
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr
|
from aqt.utils import disable_help_button, restoreGeom, saveGeom, setWindowIcon, tr
|
||||||
from aqt.webview import AnkiWebView
|
from aqt.webview import AnkiWebView
|
||||||
|
|
||||||
LastStateAndMod = tuple[str, int, int]
|
LastStateAndMod = tuple[str, int, int]
|
||||||
|
@ -52,10 +50,8 @@ class Previewer(QDialog):
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._close_callback = on_close
|
self._close_callback = on_close
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
icon = QIcon()
|
|
||||||
icon.addPixmap(QPixmap("icons:anki.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
|
||||||
disable_help_button(self)
|
disable_help_button(self)
|
||||||
self.setWindowIcon(icon)
|
setWindowIcon(self)
|
||||||
|
|
||||||
def card(self) -> Card | None:
|
def card(self) -> Card | None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
|
@ -21,7 +21,7 @@ from anki.scheduler.v3 import Scheduler as V3Scheduler
|
||||||
from anki.tags import MARKED_TAG
|
from anki.tags import MARKED_TAG
|
||||||
from anki.utils import stripHTML
|
from anki.utils import stripHTML
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.browser.card_info import CardInfoDialog
|
from aqt.browser.card_info import PreviousReviewerCardInfo, ReviewerCardInfo
|
||||||
from aqt.deckoptions import confirm_deck_then_display_options
|
from aqt.deckoptions import confirm_deck_then_display_options
|
||||||
from aqt.operations.card import set_card_flag
|
from aqt.operations.card import set_card_flag
|
||||||
from aqt.operations.note import remove_notes
|
from aqt.operations.note import remove_notes
|
||||||
|
@ -126,6 +126,8 @@ class Reviewer:
|
||||||
self._v3: V3CardInfo | None = None
|
self._v3: V3CardInfo | None = None
|
||||||
self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1))
|
self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1))
|
||||||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||||
|
self._card_info = ReviewerCardInfo(self.mw)
|
||||||
|
self._previous_card_info = PreviousReviewerCardInfo(self.mw)
|
||||||
hooks.card_did_leech.append(self.onLeech)
|
hooks.card_did_leech.append(self.onLeech)
|
||||||
|
|
||||||
def show(self) -> None:
|
def show(self) -> None:
|
||||||
|
@ -197,6 +199,9 @@ class Reviewer:
|
||||||
else:
|
else:
|
||||||
self._get_next_v3_card()
|
self._get_next_v3_card()
|
||||||
|
|
||||||
|
self._previous_card_info.set_card(self.previous_card)
|
||||||
|
self._card_info.set_card(self.card)
|
||||||
|
|
||||||
if not self.card:
|
if not self.card:
|
||||||
self.mw.moveToState("overview")
|
self.mw.moveToState("overview")
|
||||||
return
|
return
|
||||||
|
@ -958,12 +963,10 @@ time = %(time)d;
|
||||||
confirm_deck_then_display_options(self.card)
|
confirm_deck_then_display_options(self.card)
|
||||||
|
|
||||||
def on_previous_card_info(self) -> None:
|
def on_previous_card_info(self) -> None:
|
||||||
if self.previous_card:
|
self._previous_card_info.toggle()
|
||||||
CardInfoDialog(parent=self.mw, mw=self.mw, card=self.previous_card)
|
|
||||||
|
|
||||||
def on_card_info(self) -> None:
|
def on_card_info(self) -> None:
|
||||||
if self.card:
|
self._card_info.toggle()
|
||||||
CardInfoDialog(parent=self.mw, mw=self.mw, card=self.card)
|
|
||||||
|
|
||||||
def set_flag_on_current_card(self, desired_flag: int) -> None:
|
def set_flag_on_current_card(self, desired_flag: int) -> None:
|
||||||
def redraw_flag(out: OpChangesWithCount) -> None:
|
def redraw_flag(out: OpChangesWithCount) -> None:
|
||||||
|
|
|
@ -406,6 +406,12 @@ def disable_help_button(widget: QWidget) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setWindowIcon(widget: QWidget) -> None:
|
||||||
|
icon = QIcon()
|
||||||
|
icon.addPixmap(QPixmap("icons:anki.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
widget.setWindowIcon(icon)
|
||||||
|
|
||||||
|
|
||||||
# File handling
|
# File handling
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
|
@ -3,108 +3,53 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as tr2 from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { Stats, unwrapOptionalNumber } from "../lib/proto";
|
import type { Stats } from "../lib/proto";
|
||||||
import { Timestamp, timeSpan, DAY } from "../lib/time";
|
import { getCardStats } from "./lib";
|
||||||
|
import CardStats from "./CardStats.svelte";
|
||||||
import Revlog from "./Revlog.svelte";
|
import Revlog from "./Revlog.svelte";
|
||||||
|
|
||||||
export let stats: Stats.CardStatsResponse;
|
export let cardId: number | null = null;
|
||||||
|
export let includeRevlog: boolean = true;
|
||||||
|
|
||||||
function dateString(timestamp: number): string {
|
let stats: Stats.CardStatsResponse | null = null;
|
||||||
return new Timestamp(timestamp).dateString();
|
|
||||||
|
$: if (cardId === null) {
|
||||||
|
stats = null;
|
||||||
|
} else {
|
||||||
|
const requestedCardId = cardId;
|
||||||
|
getCardStats(requestedCardId).then((s) => {
|
||||||
|
/* Skip if another update has been triggered in the meantime. */
|
||||||
|
if (requestedCardId === cardId) {
|
||||||
|
stats = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatsRow {
|
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statsRows: StatsRow[] = [];
|
|
||||||
|
|
||||||
statsRows.push({ label: tr2.cardStatsAdded(), value: dateString(stats.added) });
|
|
||||||
|
|
||||||
const firstReview = unwrapOptionalNumber(stats.firstReview);
|
|
||||||
if (firstReview !== undefined) {
|
|
||||||
statsRows.push({
|
|
||||||
label: tr2.cardStatsFirstReview(),
|
|
||||||
value: dateString(firstReview),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const latestReview = unwrapOptionalNumber(stats.latestReview);
|
|
||||||
if (latestReview !== undefined) {
|
|
||||||
statsRows.push({
|
|
||||||
label: tr2.cardStatsLatestReview(),
|
|
||||||
value: dateString(latestReview),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const dueDate = unwrapOptionalNumber(stats.dueDate);
|
|
||||||
if (dueDate !== undefined) {
|
|
||||||
statsRows.push({ label: tr2.statisticsDueDate(), value: dateString(dueDate) });
|
|
||||||
}
|
|
||||||
const duePosition = unwrapOptionalNumber(stats.duePosition);
|
|
||||||
if (duePosition !== undefined) {
|
|
||||||
statsRows.push({
|
|
||||||
label: tr2.cardStatsNewCardPosition(),
|
|
||||||
value: dateString(duePosition),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stats.interval) {
|
|
||||||
statsRows.push({
|
|
||||||
label: tr2.cardStatsInterval(),
|
|
||||||
value: timeSpan(stats.interval * DAY),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (stats.ease) {
|
|
||||||
statsRows.push({ label: tr2.cardStatsEase(), value: `${stats.ease / 10}%` });
|
|
||||||
}
|
|
||||||
|
|
||||||
statsRows.push({ label: tr2.cardStatsReviewCount(), value: stats.reviews });
|
|
||||||
statsRows.push({ label: tr2.cardStatsLapseCount(), value: stats.lapses });
|
|
||||||
|
|
||||||
if (stats.totalSecs) {
|
|
||||||
statsRows.push({
|
|
||||||
label: tr2.cardStatsAverageTime(),
|
|
||||||
value: timeSpan(stats.averageSecs),
|
|
||||||
});
|
|
||||||
statsRows.push({
|
|
||||||
label: tr2.cardStatsTotalTime(),
|
|
||||||
value: timeSpan(stats.totalSecs),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
statsRows.push({ label: tr2.cardStatsCardTemplate(), value: stats.cardType });
|
|
||||||
statsRows.push({ label: tr2.cardStatsNoteType(), value: stats.notetype });
|
|
||||||
statsRows.push({ label: tr2.cardStatsDeckName(), value: stats.deck });
|
|
||||||
|
|
||||||
statsRows.push({ label: tr2.cardStatsCardId(), value: stats.cardId });
|
|
||||||
statsRows.push({ label: tr2.cardStatsNoteId(), value: stats.noteId });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if stats !== null}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div>
|
<div>
|
||||||
<table class="stats-table">
|
<CardStats {stats} />
|
||||||
{#each statsRows as row, _index}
|
{#if includeRevlog}
|
||||||
<tr>
|
|
||||||
<th style="text-align:start">{row.label}</th>
|
|
||||||
<td>{row.value}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</table>
|
|
||||||
<Revlog {stats} />
|
<Revlog {stats} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="placeholder">{tr.cardStatsNoCard()}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
max-width: 40em;
|
max-width: 40em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-table {
|
.placeholder {
|
||||||
width: 100%;
|
margin: 0;
|
||||||
border-spacing: 1em 0;
|
position: absolute;
|
||||||
border-collapse: collapse;
|
top: 50%;
|
||||||
text-align: start;
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
113
ts/card-info/CardStats.svelte
Normal file
113
ts/card-info/CardStats.svelte
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import * as tr2 from "../lib/ftl";
|
||||||
|
import { Stats, unwrapOptionalNumber } from "../lib/proto";
|
||||||
|
import { Timestamp, timeSpan, DAY } from "../lib/time";
|
||||||
|
|
||||||
|
export let stats: Stats.CardStatsResponse;
|
||||||
|
|
||||||
|
function dateString(timestamp: number): string {
|
||||||
|
return new Timestamp(timestamp).dateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatsRow {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowsFromStats(stats: Stats.CardStatsResponse): StatsRow[] {
|
||||||
|
const statsRows: StatsRow[] = [];
|
||||||
|
|
||||||
|
statsRows.push({ label: tr2.cardStatsAdded(), value: dateString(stats.added) });
|
||||||
|
|
||||||
|
const firstReview = unwrapOptionalNumber(stats.firstReview);
|
||||||
|
if (firstReview !== undefined) {
|
||||||
|
statsRows.push({
|
||||||
|
label: tr2.cardStatsFirstReview(),
|
||||||
|
value: dateString(firstReview),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const latestReview = unwrapOptionalNumber(stats.latestReview);
|
||||||
|
if (latestReview !== undefined) {
|
||||||
|
statsRows.push({
|
||||||
|
label: tr2.cardStatsLatestReview(),
|
||||||
|
value: dateString(latestReview),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dueDate = unwrapOptionalNumber(stats.dueDate);
|
||||||
|
if (dueDate !== undefined) {
|
||||||
|
statsRows.push({
|
||||||
|
label: tr2.statisticsDueDate(),
|
||||||
|
value: dateString(dueDate),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const duePosition = unwrapOptionalNumber(stats.duePosition);
|
||||||
|
if (duePosition !== undefined) {
|
||||||
|
statsRows.push({
|
||||||
|
label: tr2.cardStatsNewCardPosition(),
|
||||||
|
value: dateString(duePosition),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.interval) {
|
||||||
|
statsRows.push({
|
||||||
|
label: tr2.cardStatsInterval(),
|
||||||
|
value: timeSpan(stats.interval * DAY),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (stats.ease) {
|
||||||
|
statsRows.push({
|
||||||
|
label: tr2.cardStatsEase(),
|
||||||
|
value: `${stats.ease / 10}%`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
statsRows.push({ label: tr2.cardStatsReviewCount(), value: stats.reviews });
|
||||||
|
statsRows.push({ label: tr2.cardStatsLapseCount(), value: stats.lapses });
|
||||||
|
|
||||||
|
if (stats.totalSecs) {
|
||||||
|
statsRows.push({
|
||||||
|
label: tr2.cardStatsAverageTime(),
|
||||||
|
value: timeSpan(stats.averageSecs),
|
||||||
|
});
|
||||||
|
statsRows.push({
|
||||||
|
label: tr2.cardStatsTotalTime(),
|
||||||
|
value: timeSpan(stats.totalSecs),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
statsRows.push({ label: tr2.cardStatsCardTemplate(), value: stats.cardType });
|
||||||
|
statsRows.push({ label: tr2.cardStatsNoteType(), value: stats.notetype });
|
||||||
|
statsRows.push({ label: tr2.cardStatsDeckName(), value: stats.deck });
|
||||||
|
|
||||||
|
statsRows.push({ label: tr2.cardStatsCardId(), value: stats.cardId });
|
||||||
|
statsRows.push({ label: tr2.cardStatsNoteId(), value: stats.noteId });
|
||||||
|
|
||||||
|
return statsRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
let statsRows: StatsRow[];
|
||||||
|
$: statsRows = rowsFromStats(stats);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<table class="stats-table">
|
||||||
|
{#each statsRows as row, _index}
|
||||||
|
<tr>
|
||||||
|
<th style="text-align:start">{row.label}</th>
|
||||||
|
<td>{row.value}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.stats-table {
|
||||||
|
width: 100%;
|
||||||
|
border-spacing: 1em 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -72,9 +72,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const revlogRows: RevlogRow[] = stats.revlog.map((entry) =>
|
let revlogRows: RevlogRow[];
|
||||||
revlogRowFromEntry(entry)
|
$: revlogRows = stats.revlog.map((entry) => revlogRowFromEntry(entry));
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if stats.revlog.length}
|
{#if stats.revlog.length}
|
||||||
|
|
|
@ -1,33 +1,15 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { getCardStats } from "./lib";
|
|
||||||
import { setupI18n, ModuleName } from "../lib/i18n";
|
import { setupI18n, ModuleName } from "../lib/i18n";
|
||||||
import { checkNightMode } from "../lib/nightmode";
|
import { checkNightMode } from "../lib/nightmode";
|
||||||
|
|
||||||
import CardInfo from "./CardInfo.svelte";
|
import CardInfo from "./CardInfo.svelte";
|
||||||
|
|
||||||
export async function cardInfo(
|
export async function cardInfo(target: HTMLDivElement): Promise<CardInfo> {
|
||||||
target: HTMLDivElement,
|
|
||||||
cardId: number,
|
|
||||||
includeRevlog: boolean
|
|
||||||
): Promise<CardInfo> {
|
|
||||||
checkNightMode();
|
checkNightMode();
|
||||||
const [stats] = await Promise.all([
|
await setupI18n({
|
||||||
getCardStats(cardId),
|
modules: [ModuleName.CARD_STATS, ModuleName.SCHEDULING, ModuleName.STATISTICS],
|
||||||
setupI18n({
|
|
||||||
modules: [
|
|
||||||
ModuleName.CARD_STATS,
|
|
||||||
ModuleName.SCHEDULING,
|
|
||||||
ModuleName.STATISTICS,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
if (!includeRevlog) {
|
|
||||||
stats.revlog = [];
|
|
||||||
}
|
|
||||||
return new CardInfo({
|
|
||||||
target,
|
|
||||||
props: { stats },
|
|
||||||
});
|
});
|
||||||
|
return new CardInfo({ target });
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue