# Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from typing import Any import aqt import aqt.operations from anki.collection import OpChanges from anki.scheduler import UnburyDeck from aqt import gui_hooks from aqt.deckdescription import DeckDescriptionDialog from aqt.deckoptions import display_options_for_deck from aqt.operations import QueryOp from aqt.operations.scheduling import ( empty_filtered_deck, rebuild_filtered_deck, unbury_deck, ) from aqt.sound import av_player from aqt.toolbar import BottomBar from aqt.utils import askUserDialog, openLink, shortcut, tooltip, tr from aqt.qt import QLabel, QHBoxLayout, QWidget class OverviewBottomBar: def __init__(self, overview: Overview) -> None: self.overview = overview @dataclass class OverviewContent: deck: str shareLink: str desc: str table: str class Overview: "Deck overview." def __init__(self, mw: aqt.AnkiQt) -> None: self.mw = mw self.web = mw.web self.bottom = BottomBar(mw, mw.bottomWeb) self._refresh_needed = False self.streak_label = QLabel() self.streak_label.setText("") self.streak_label.setStyleSheet( """ font-size: 16px; padding: 10px; color: orange; font-weight: bold; qproperty-alignment: AlignCenter; """ ) streak_layout = QHBoxLayout() streak_layout.addStretch() streak_layout.addWidget(self.streak_label) streak_layout.addStretch() streak_widget = QWidget() streak_widget.setLayout(streak_layout) layout = self.web.layout() if layout is not None: layout = self.web.layout() if layout is not None and hasattr(layout, "insertWidget"): layout.insertWidget(0, streak_widget) def show(self) -> None: av_player.stop_and_clear_queue() self.web.set_bridge_command(self._linkHandler, self) self.mw.setStateShortcuts(self._shortcutKeys()) self.refresh() def refresh(self) -> None: def success(_counts: tuple) -> None: self._refresh_needed = False self._renderPage() self._renderBottom() self.mw.web.setFocus() streak_days = 0 if self.mw.col and self.mw.col.db: streak_days = self.mw.col.db.scalar( """ SELECT COUNT(*) FROM ( SELECT id FROM revlog WHERE id > strftime('%s', 'now', '-30 days')*1000 GROUP BY strftime('%Y-%m-%d', id/1000, 'unixepoch') ) """ ) self.streak_label.setText(f" Streak: {streak_days} Tage") gui_hooks.overview_did_refresh(self) QueryOp( parent=self.mw, op=lambda col: col.sched.counts(), success=success ).run_in_background() def refresh_if_needed(self) -> None: if self._refresh_needed: self.refresh() def op_executed( self, changes: OpChanges, handler: object | None, focused: bool ) -> bool: if changes.study_queues: self._refresh_needed = True if focused: self.refresh_if_needed() return self._refresh_needed def _linkHandler(self, url: str) -> bool: if url == "study": self.mw.col.startTimebox() self.mw.moveToState("review") if self.mw.state == "overview": tooltip(tr.studying_no_cards_are_due_yet()) elif url == "anki": print("anki menu") elif url == "opts": display_options_for_deck(self.mw.col.decks.current()) elif url == "cram": aqt.dialogs.open("FilteredDeckConfigDialog", self.mw) elif url == "refresh": self.rebuild_current_filtered_deck() elif url == "empty": self.empty_current_filtered_deck() elif url == "decks": self.mw.moveToState("deckBrowser") elif url == "review": openLink(f"{aqt.appShared}info/{self.sid}?v={self.sidVer}") elif url == "studymore" or url == "customStudy": self.onStudyMore() elif url == "unbury": self.on_unbury() elif url == "description": self.edit_description() elif url.lower().startswith("http"): openLink(url) return False def _shortcutKeys(self) -> list[tuple[str, Callable]]: return [ ("o", lambda: display_options_for_deck(self.mw.col.decks.current())), ("r", self.rebuild_current_filtered_deck), ("e", self.empty_current_filtered_deck), ("c", self.onCustomStudyKey), ("u", self.on_unbury), ] def _current_deck_is_filtered(self) -> int: return self.mw.col.decks.current()["dyn"] def rebuild_current_filtered_deck(self) -> None: rebuild_filtered_deck( parent=self.mw, deck_id=self.mw.col.decks.selected() ).run_in_background() def empty_current_filtered_deck(self) -> None: empty_filtered_deck( parent=self.mw, deck_id=self.mw.col.decks.selected() ).run_in_background() def onCustomStudyKey(self) -> None: if not self._current_deck_is_filtered(): self.onStudyMore() def on_unbury(self) -> None: mode = UnburyDeck.Mode.ALL info = self.mw.col.sched.congratulations_info() if info.have_sched_buried and info.have_user_buried: opts = [ tr.studying_manually_buried_cards(), tr.studying_buried_siblings(), tr.studying_all_buried_cards(), tr.actions_cancel(), ] diag = askUserDialog(tr.studying_what_would_you_like_to_unbury(), opts) diag.setDefault(0) ret = diag.run() if ret == opts[0]: mode = UnburyDeck.Mode.USER_ONLY elif ret == opts[1]: mode = UnburyDeck.Mode.SCHED_ONLY elif ret == opts[3]: return unbury_deck( parent=self.mw, deck_id=self.mw.col.decks.get_current_id(), mode=mode ).run_in_background() onUnbury = on_unbury def _renderPage(self) -> None: but = self.mw.button deck = self.mw.col.decks.current() self.sid = deck.get("sharedFrom") if self.sid: self.sidVer = deck.get("ver", None) shareLink = 'Reviews and Updates' else: shareLink = "" if self.mw.col.sched._is_finished(): self._show_finished_screen() return content = OverviewContent( deck=deck["name"], shareLink=shareLink, desc=self._desc(deck), table=self._table(), ) gui_hooks.overview_will_render_content(self, content) self.web.stdHtml( self._body % content.__dict__, css=["css/overview.css"], js=["js/vendor/jquery.min.js"], context=self, ) def _show_finished_screen(self) -> None: self.web.load_sveltekit_page("congrats") def _desc(self, deck: dict[str, Any]) -> str: if deck["dyn"]: desc = tr.studying_this_is_a_special_deck_for() desc += f" {tr.studying_cards_will_be_automatically_returned_to()}" desc += f" {tr.studying_deleting_this_deck_from_the_deck()}" else: desc = deck.get("desc", "") if deck.get("md", False): desc = self.mw.col.render_markdown(desc) if not desc: return "

" dyn = "dyn" if deck["dyn"] else "" return f'

{desc}
' def _table(self) -> str: counts = list(self.mw.col.sched.counts()) current_did = self.mw.col.decks.get_current_id() deck_node = self.mw.col.sched.deck_due_tree(current_did) but = self.mw.button if self.mw.col.v3_scheduler(): assert deck_node is not None buried_new = deck_node.new_count - counts[0] buried_learning = deck_node.learn_count - counts[1] buried_review = deck_node.review_count - counts[2] else: buried_new = buried_learning = buried_review = 0 buried_label = tr.studying_counts_differ() def number_row(title: str, klass: str, count: int, buried_count: int) -> str: buried = f"{buried_count:+}" if buried_count else "" return f""" {title}: {count} {buried} """ return f"""
{number_row(tr.actions_new(), "new-count", counts[0], buried_new)} {number_row(tr.scheduling_learning(), "learn-count", counts[1], buried_learning)} {number_row(tr.studying_to_review(), "review-count", counts[2], buried_review)}
{but("study", tr.studying_study_now(), id="study", extra=" autofocus")}
""" _body = """

%(deck)s

%(shareLink)s %(desc)s %(table)s
""" def edit_description(self) -> None: DeckDescriptionDialog(self.mw) def _renderBottom(self) -> None: links = [["O", "opts", tr.actions_options()]] is_dyn = self.mw.col.decks.current()["dyn"] if is_dyn: links.append(["R", "refresh", tr.actions_rebuild()]) links.append(["E", "empty", tr.studying_empty()]) else: links.append(["C", "studymore", tr.actions_custom_study()]) if self.mw.col.sched.have_buried(): links.append(["U", "unbury", tr.studying_unbury()]) if not is_dyn: links.append(["", "description", tr.scheduling_description()]) link_handler = gui_hooks.overview_will_render_bottom(self._linkHandler, links) if not callable(link_handler): link_handler = self._linkHandler buf = "" for b in links: if b[0]: b[0] = tr.actions_shortcut_key(val=shortcut(b[0])) buf += f""" """ self.bottom.draw( buf=buf, link_handler=link_handler, web_context=OverviewBottomBar(self) ) def onStudyMore(self) -> None: import aqt.customstudy aqt.customstudy.CustomStudy.fetch_data_and_show(self.mw)