diff --git a/ftl/core/card-stats.ftl b/ftl/core/card-stats.ftl
index af08defcd..82a0dbee2 100644
--- a/ftl/core/card-stats.ftl
+++ b/ftl/core/card-stats.ftl
@@ -22,3 +22,9 @@ card-stats-review-log-type-review = Review
card-stats-review-log-type-relearn = Relearn
card-stats-review-log-type-filtered = Filtered
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 })
diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py
index 2b9cd01d1..013f6088c 100644
--- a/pylib/anki/stats.py
+++ b/pylib/anki/stats.py
@@ -28,6 +28,7 @@ def _legacy_card_stats(
) -> str:
"A quick hack to preserve compatibility with the old HTML string API."
random_id = f"cardinfo-{base62(random.randint(0, 2 ** 64 - 1))}"
+ varName = random_id.replace("-", "")
return f"""
@@ -38,7 +39,8 @@ def _legacy_card_stats(
if ({1 if _legacy_nightmode else 0}) {{
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()} }}));
"""
diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py
index 1c90beeaf..4b66b2ef9 100644
--- a/qt/aqt/browser/browser.py
+++ b/qt/aqt/browser/browser.py
@@ -58,7 +58,7 @@ from aqt.utils import (
)
from ..changenotetype import change_notetype_dialog
-from .card_info import CardInfoDialog
+from .card_info import BrowserCardInfo
from .find_and_replace import FindAndReplaceDialog
from .previewer import BrowserPreviewer as PreviewDialog
from .previewer import Previewer
@@ -110,6 +110,7 @@ class Browser(QMainWindow):
self.lastFilter = ""
self.focusTo: int | None = None
self._previewer: Previewer | None = None
+ self._card_info = BrowserCardInfo(self.mw)
self._closeEventHasCleanedUp = False
self.form = aqt.forms.browser.Ui_Dialog()
self.form.setupUi(self)
@@ -155,6 +156,7 @@ class Browser(QMainWindow):
if changes.browser_table and changes.card:
self.card = self.table.get_single_selected_card()
self.current_card = self.table.get_current_card()
+ self._update_card_info()
self._update_current_actions()
# changes.card is required for updating flag icon
@@ -236,6 +238,7 @@ class Browser(QMainWindow):
def _closeWindow(self) -> None:
self._cleanup_preview()
+ self._card_info.close()
self.editor.cleanup()
self.table.cleanup()
self.sidebar.cleanup()
@@ -447,6 +450,7 @@ class Browser(QMainWindow):
return
self.current_card = self.table.get_current_card()
self._update_current_actions()
+ self._update_card_info()
def _update_row_actions(self) -> None:
has_rows = bool(self.table.len())
@@ -545,10 +549,10 @@ class Browser(QMainWindow):
######################################################################
def showCardInfo(self) -> None:
- if not self.current_card:
- return
+ self._card_info.toggle()
- 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
######################################################################
diff --git a/qt/aqt/browser/card_info.py b/qt/aqt/browser/card_info.py
index b059bfaa5..61bdd2274 100644
--- a/qt/aqt/browser/card_info.py
+++ b/qt/aqt/browser/card_info.py
@@ -3,8 +3,12 @@
from __future__ import annotations
+import json
+from typing import Callable
+
import aqt
from anki.cards import Card, CardId
+from anki.lang import without_unicode_isolation
from aqt.qt import *
from aqt.utils import (
addCloseShortcut,
@@ -12,6 +16,8 @@ from aqt.utils import (
qconnect,
restoreGeom,
saveGeom,
+ setWindowIcon,
+ tr,
)
from aqt.webview import AnkiWebView
@@ -21,18 +27,30 @@ class CardInfoDialog(QDialog):
GEOMETRY_KEY = "revlog"
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)
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()
- def _setup_ui(self, card_id: CardId) -> None:
- self.setWindowModality(Qt.WindowModality.ApplicationModal)
+ def _setup_ui(self, card_id: CardId | None) -> None:
self.mw.garbage_collect_on_dialog_finish(self)
disable_help_button(self)
restoreGeom(self, self.GEOMETRY_KEY)
addCloseShortcut(self)
+ setWindowIcon(self)
self.web = AnkiWebView(title=self.TITLE)
self.web.setVisible(False)
@@ -47,10 +65,87 @@ class CardInfoDialog(QDialog):
self.setLayout(layout)
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:
+ if self._on_close:
+ self._on_close()
self.web = None
saveGeom(self, self.GEOMETRY_KEY)
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())
+ ),
+ )
diff --git a/qt/aqt/browser/previewer.py b/qt/aqt/browser/previewer.py
index 190d65ecb..01b6c253f 100644
--- a/qt/aqt/browser/previewer.py
+++ b/qt/aqt/browser/previewer.py
@@ -17,9 +17,7 @@ from aqt.qt import (
QCheckBox,
QDialog,
QDialogButtonBox,
- QIcon,
QKeySequence,
- QPixmap,
QShortcut,
Qt,
QTimer,
@@ -30,7 +28,7 @@ from aqt.qt import (
from aqt.reviewer import replay_audio
from aqt.sound import av_player, play_clicked_audio
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
LastStateAndMod = tuple[str, int, int]
@@ -52,10 +50,8 @@ class Previewer(QDialog):
self._parent = parent
self._close_callback = on_close
self.mw = mw
- icon = QIcon()
- icon.addPixmap(QPixmap("icons:anki.png"), QIcon.Mode.Normal, QIcon.State.Off)
disable_help_button(self)
- self.setWindowIcon(icon)
+ setWindowIcon(self)
def card(self) -> Card | None:
raise NotImplementedError
diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py
index afc309655..47167363d 100644
--- a/qt/aqt/reviewer.py
+++ b/qt/aqt/reviewer.py
@@ -21,7 +21,7 @@ from anki.scheduler.v3 import Scheduler as V3Scheduler
from anki.tags import MARKED_TAG
from anki.utils import stripHTML
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.operations.card import set_card_flag
from aqt.operations.note import remove_notes
@@ -126,6 +126,8 @@ class Reviewer:
self._v3: V3CardInfo | None = None
self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1))
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)
def show(self) -> None:
@@ -197,6 +199,9 @@ class Reviewer:
else:
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:
self.mw.moveToState("overview")
return
@@ -958,12 +963,10 @@ time = %(time)d;
confirm_deck_then_display_options(self.card)
def on_previous_card_info(self) -> None:
- if self.previous_card:
- CardInfoDialog(parent=self.mw, mw=self.mw, card=self.previous_card)
+ self._previous_card_info.toggle()
def on_card_info(self) -> None:
- if self.card:
- CardInfoDialog(parent=self.mw, mw=self.mw, card=self.card)
+ self._card_info.toggle()
def set_flag_on_current_card(self, desired_flag: int) -> None:
def redraw_flag(out: OpChangesWithCount) -> None:
diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py
index ab3163e9e..28f1242dc 100644
--- a/qt/aqt/utils.py
+++ b/qt/aqt/utils.py
@@ -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
######################################################################
diff --git a/ts/card-info/CardInfo.svelte b/ts/card-info/CardInfo.svelte
index c0c980f59..41d6a7ce9 100644
--- a/ts/card-info/CardInfo.svelte
+++ b/ts/card-info/CardInfo.svelte
@@ -3,108 +3,53 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
-
-
-
- {#each statsRows as row, _index}
-
- {row.label} |
- {row.value} |
-
- {/each}
-
-
+{#if stats !== null}
+
+
+
+ {#if includeRevlog}
+
+ {/if}
+
-
+{:else}
+
{tr.cardStatsNoCard()}
+{/if}
diff --git a/ts/card-info/CardStats.svelte b/ts/card-info/CardStats.svelte
new file mode 100644
index 000000000..ccaef6672
--- /dev/null
+++ b/ts/card-info/CardStats.svelte
@@ -0,0 +1,113 @@
+
+
+
+
+ {#each statsRows as row, _index}
+
+ {row.label} |
+ {row.value} |
+
+ {/each}
+
+
+
diff --git a/ts/card-info/Revlog.svelte b/ts/card-info/Revlog.svelte
index b5e6f6f0b..f8ec555c4 100644
--- a/ts/card-info/Revlog.svelte
+++ b/ts/card-info/Revlog.svelte
@@ -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) =>
- revlogRowFromEntry(entry)
- );
+ let revlogRows: RevlogRow[];
+ $: revlogRows = stats.revlog.map((entry) => revlogRowFromEntry(entry));
{#if stats.revlog.length}
diff --git a/ts/card-info/index.ts b/ts/card-info/index.ts
index 61cd7d9ac..2473c285f 100644
--- a/ts/card-info/index.ts
+++ b/ts/card-info/index.ts
@@ -1,33 +1,15 @@
// Copyright: Ankitects Pty Ltd and contributors
// 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 { checkNightMode } from "../lib/nightmode";
import CardInfo from "./CardInfo.svelte";
-export async function cardInfo(
- target: HTMLDivElement,
- cardId: number,
- includeRevlog: boolean
-): Promise
{
+export async function cardInfo(target: HTMLDivElement): Promise {
checkNightMode();
- const [stats] = await Promise.all([
- getCardStats(cardId),
- setupI18n({
- modules: [
- ModuleName.CARD_STATS,
- ModuleName.SCHEDULING,
- ModuleName.STATISTICS,
- ],
- }),
- ]);
- if (!includeRevlog) {
- stats.revlog = [];
- }
- return new CardInfo({
- target,
- props: { stats },
+ await setupI18n({
+ modules: [ModuleName.CARD_STATS, ModuleName.SCHEDULING, ModuleName.STATISTICS],
});
+ return new CardInfo({ target });
}