Merge pull request #1431 from RumovZ/non-modal-card-info

Non modal card info
This commit is contained in:
Damien Elmes 2021-10-18 18:09:52 +10:00 committed by GitHub
commit 5eb1208714
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 285 additions and 134 deletions

View File

@ -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 })

View File

@ -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"""
<div id="{random_id}"></div>
<script src="js/vendor/bootstrap.bundle.min.js"></script>
@ -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()} }}));
</script>
"""

View File

@ -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
######################################################################

View File

@ -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())
),
)

View File

@ -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

View File

@ -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:

View File

@ -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
######################################################################

View File

@ -3,108 +3,53 @@ 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";
import * as tr from "../lib/ftl";
import type { Stats } from "../lib/proto";
import { getCardStats } from "./lib";
import CardStats from "./CardStats.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 {
return new Timestamp(timestamp).dateString();
}
let stats: Stats.CardStatsResponse | null = null;
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),
$: 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;
}
});
}
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>
<div class="container">
<div>
<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>
<Revlog {stats} />
{#if stats !== null}
<div class="container">
<div>
<CardStats {stats} />
{#if includeRevlog}
<Revlog {stats} />
{/if}
</div>
</div>
</div>
{:else}
<div class="placeholder">{tr.cardStatsNoCard()}</div>
{/if}
<style>
.container {
max-width: 40em;
}
.stats-table {
width: 100%;
border-spacing: 1em 0;
border-collapse: collapse;
text-align: start;
.placeholder {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

View 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>

View File

@ -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));
</script>
{#if stats.revlog.length}

View File

@ -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<CardInfo> {
export async function cardInfo(target: HTMLDivElement): Promise<CardInfo> {
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 });
}