diff --git a/proto/backend.proto b/proto/backend.proto
index 01346b02d..7a3fdc7e9 100644
--- a/proto/backend.proto
+++ b/proto/backend.proto
@@ -92,10 +92,10 @@ service BackendService {
rpc SetLocalMinutesWest (Int32) returns (Empty);
rpc SchedTimingToday (Empty) returns (SchedTimingTodayOut);
rpc StudiedToday (StudiedTodayIn) returns (String);
- rpc CongratsLearnMessage (CongratsLearnMessageIn) returns (String);
rpc UpdateStats (UpdateStatsIn) returns (Empty);
rpc ExtendLimits (ExtendLimitsIn) returns (Empty);
rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut);
+ rpc CongratsInfo (Empty) returns (CongratsInfoOut);
// stats
@@ -1016,3 +1016,14 @@ message RevlogEntry {
uint32 taken_millis = 8;
ReviewKind review_kind = 9;
}
+
+message CongratsInfoOut {
+ uint32 learn_remaining = 1;
+ uint32 secs_until_next_learn = 2;
+ bool review_remaining = 3;
+ bool new_remaining = 4;
+ bool have_sched_buried = 5;
+ bool have_user_buried = 6;
+ bool is_filtered_deck = 7;
+ bool bridge_commands_supported = 8;
+}
diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py
index 58ecb9669..ff817c903 100644
--- a/pylib/anki/rsbackend.py
+++ b/pylib/anki/rsbackend.py
@@ -9,6 +9,9 @@ Please do not access methods on the backend directly - they may be changed
or removed at any time. Instead, please use the methods on the collection
instead. Eg, don't use col.backend.all_deck_config(), instead use
col.decks.all_config()
+
+If you need to access a backend method that is not currently accessible
+via the collection, please send through a pull request that adds a method.
"""
from __future__ import annotations
diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py
index 162efbb24..6eb23c81d 100644
--- a/pylib/anki/schedv2.py
+++ b/pylib/anki/schedv2.py
@@ -10,6 +10,7 @@ from heapq import *
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Union
import anki # pylint: disable=unused-import
+import anki.backend_pb2 as pb
from anki import hooks
from anki.cards import Card
from anki.consts import *
@@ -1281,118 +1282,26 @@ where id = ?
# Deck finished state
##########################################################################
- def finishedMsg(self) -> str:
- return (
- ""
- + _("Congratulations! You have finished this deck for now.")
- + "
"
- + self._nextDueMsg()
- )
+ def congratulations_info(self) -> pb.CongratsInfoOut:
+ return self.col.backend.congrats_info()
- def next_learn_msg(self) -> str:
- dids = self._deckLimit()
- (next, remaining) = self.col.db.first(
- f"""
-select min(due), count(*)
-from cards where did in {dids} and queue = {QUEUE_TYPE_LRN}
-"""
- )
- next = next or 0
- remaining = remaining or 0
- if next and next < self.dayCutoff:
- next -= intTime() + self.col.conf["collapseTime"]
- return self.col.backend.congrats_learn_message(
- next_due=abs(next), remaining=remaining
- )
- else:
- return ""
+ def finishedMsg(self) -> str:
+ print("finishedMsg() is obsolete")
+ return ""
def _nextDueMsg(self) -> str:
- line = []
-
- learn_msg = self.next_learn_msg()
- if learn_msg:
- line.append(learn_msg)
-
- # the new line replacements are so we don't break translations
- # in a point release
- if self.revDue():
- line.append(
- _(
- """\
-Today's review limit has been reached, but there are still cards
-waiting to be reviewed. For optimum memory, consider increasing
-the daily limit in the options."""
- ).replace("\n", " ")
- )
- if self.newDue():
- line.append(
- _(
- """\
-There are more new cards available, but the daily limit has been
-reached. You can increase the limit in the options, but please
-bear in mind that the more new cards you introduce, the higher
-your short-term review workload will become."""
- ).replace("\n", " ")
- )
- if self.haveBuried():
- if self.haveCustomStudy:
- now = " " + _("To see them now, click the Unbury button below.")
- else:
- now = ""
- line.append(
- _(
- """\
-Some related or buried cards were delayed until a later session."""
- )
- + now
- )
- if self.haveCustomStudy and not self.col.decks.current()["dyn"]:
- line.append(
- _(
- """\
-To study outside of the normal schedule, click the Custom Study button below."""
- )
- )
- return "
".join(line) - - def revDue(self) -> Optional[int]: - "True if there are any rev cards due." - return self.col.db.scalar( - ( - f"select 1 from cards where did in %s and queue = {QUEUE_TYPE_REV} " - "and due <= ? limit 1" - ) - % self._deckLimit(), - self.today, - ) - - def newDue(self) -> Optional[int]: - "True if there are any new cards due." - return self.col.db.scalar( - ( - f"select 1 from cards where did in %s and queue = {QUEUE_TYPE_NEW} " - "limit 1" - ) - % self._deckLimit() - ) + print("_nextDueMsg() is obsolete") + return "" def haveBuriedSiblings(self) -> bool: - cnt = self.col.db.scalar( - f"select 1 from cards where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s limit 1" - % self._deckLimit() - ) - return not not cnt + return self.congratulations_info().have_sched_buried def haveManuallyBuried(self) -> bool: - cnt = self.col.db.scalar( - f"select 1 from cards where queue = {QUEUE_TYPE_MANUALLY_BURIED} and did in %s limit 1" - % self._deckLimit() - ) - return not not cnt + return self.congratulations_info().have_user_buried def haveBuried(self) -> bool: - return self.haveManuallyBuried() or self.haveBuriedSiblings() + info = self.congratulations_info() + return info.have_sched_buried or info.have_user_buried # Next time reports ########################################################################## diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index a72e5da90..dc33a86ea 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -433,27 +433,6 @@ def test_overdue_lapse(): assert col.sched.counts() == (0, 0, 1) -def test_finished(): - col = getEmptyCol() - # nothing due - assert "Congratulations" in col.sched.finishedMsg() - assert "limit" not in col.sched.finishedMsg() - note = col.newNote() - note["Front"] = "one" - note["Back"] = "two" - col.addNote(note) - # have a new card - assert "new cards available" in col.sched.finishedMsg() - # turn it into a review - col.reset() - c = note.cards()[0] - c.startTimer() - col.sched.answerCard(c, 3) - # nothing should be due tomorrow, as it's due in a week - assert "Congratulations" in col.sched.finishedMsg() - assert "limit" not in col.sched.finishedMsg() - - def test_nextIvl(): col = getEmptyCol() note = col.newNote() diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 51c2c1b3f..2c4d9c3f2 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -525,27 +525,6 @@ def test_overdue_lapse(): assert col.sched.counts() == (0, 0, 1) -def test_finished(): - col = getEmptyCol() - # nothing due - assert "Congratulations" in col.sched.finishedMsg() - assert "limit" not in col.sched.finishedMsg() - note = col.newNote() - note["Front"] = "one" - note["Back"] = "two" - col.addNote(note) - # have a new card - assert "new cards available" in col.sched.finishedMsg() - # turn it into a review - col.reset() - c = note.cards()[0] - c.startTimer() - col.sched.answerCard(c, 3) - # nothing should be due tomorrow, as it's due in a week - assert "Congratulations" in col.sched.finishedMsg() - assert "limit" not in col.sched.finishedMsg() - - def test_nextIvl(): col = getEmptyCol() note = col.newNote() diff --git a/qt/.gitignore b/qt/.gitignore index 43f4d25ac..b38578942 100644 --- a/qt/.gitignore +++ b/qt/.gitignore @@ -21,6 +21,7 @@ aqt_data/web/reviewer.js aqt_data/web/webview.js aqt_data/web/toolbar.js aqt_data/web/graphs* +aqt_data/web/congrats* aqt_data/web/*.css dist aqt.egg-info diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index d7294f018..880455fc2 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -15,11 +15,10 @@ from http import HTTPStatus import flask import flask_cors # type: ignore -from flask import request +from flask import Response, request from waitress.server import create_server import aqt -from anki.collection import Collection from anki.rsbackend import from_json_bytes from anki.utils import devMode from aqt.qt import * @@ -127,24 +126,7 @@ def allroutes(pathin): try: if flask.request.method == "POST": - if not aqt.mw.col: - print(f"collection not open, ignore request for {path}") - return flask.make_response("Collection not open", HTTPStatus.NOT_FOUND) - - if path == "graphData": - body = request.data - data = graph_data(aqt.mw.col, **from_json_bytes(body)) - elif path == "i18nResources": - data = aqt.mw.col.backend.i18n_resources() - else: - return flask.make_response( - "Post request to '%s - %s' is a security leak!" % (directory, path), - HTTPStatus.FORBIDDEN, - ) - - response = flask.make_response(data) - response.headers["Content-Type"] = "application/binary" - return response + return handle_post(path) if fullpath.endswith(".css"): # some users may have invalid mime type in the Windows registry @@ -219,5 +201,33 @@ def _redirectWebExports(path): return aqt.mw.col.media.dir(), path -def graph_data(col: Collection, search: str, days: int) -> bytes: - return col.backend.graphs(search=search, days=days) +def graph_data() -> bytes: + args = from_json_bytes(request.data) + return aqt.mw.col.backend.graphs(search=args["search"], days=args["days"]) + + +def congrats_info() -> bytes: + info = aqt.mw.col.backend.congrats_info() + return info.SerializeToString() + + +post_handlers = dict( + graphData=graph_data, + # pylint: disable=unnecessary-lambda + i18nResources=lambda: aqt.mw.col.backend.i18n_resources(), + congratsInfo=congrats_info, +) + + +def handle_post(path: str) -> Response: + if not aqt.mw.col: + print(f"collection not open, ignore request for {path}") + return flask.make_response("Collection not open", HTTPStatus.NOT_FOUND) + + if path in post_handlers: + data = post_handlers[path]() + response = flask.make_response(data) + response.headers["Content-Type"] = "application/binary" + return response + else: + return flask.make_response(f"Unhandled post to {path}", HTTPStatus.FORBIDDEN,) diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 52a7008c1..4881d3d6c 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -5,11 +5,14 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Optional import aqt from anki.lang import _ from aqt import gui_hooks +from aqt.qt import QUrl from aqt.sound import av_player +from aqt.theme import theme_manager from aqt.toolbar import BottomBar from aqt.utils import askUserDialog, openLink, shortcut, tooltip @@ -84,7 +87,7 @@ class Overview: self.mw.moveToState("deckBrowser") elif url == "review": openLink(aqt.appShared + "info/%s?v=%s" % (self.sid, self.sidVer)) - elif url == "studymore": + elif url == "studymore" or url == "customStudy": self.onStudyMore() elif url == "unbury": self.onUnbury() @@ -161,6 +164,11 @@ class Overview: shareLink = 'Reviews and Updates' else: shareLink = "" + table_text = self._table() + if not table_text: + # deck is finished + self._show_finished_screen() + return content = OverviewContent( deck=deck["name"], shareLink=shareLink, @@ -175,6 +183,16 @@ class Overview: context=self, ) + def _show_finished_screen(self): + self.web.set_open_links_externally(False) + if theme_manager.night_mode: + extra = "#night" + else: + extra = "" + self.web.hide_while_preserving_layout() + self.web.load(QUrl(f"{self.mw.serverURL()}_anki/congrats.html" + extra)) + self.web.inject_dynamic_style_and_show() + def _desc(self, deck): if deck["dyn"]: desc = _( @@ -201,14 +219,13 @@ to their original deck.""" dyn = "" return '