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} - - - - - {/each} -
{row.label}{row.value}
- +{#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} + + + + + {/each} +
{row.label}{row.value}
+ + 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 }); }