diff --git a/ftl/core/custom-study.ftl b/ftl/core/custom-study.ftl
index 7355ae166..254338546 100644
--- a/ftl/core/custom-study.ftl
+++ b/ftl/core/custom-study.ftl
@@ -10,7 +10,6 @@ custom-study-increase-todays-new-card-limit = Increase today's new card limit
custom-study-increase-todays-new-card-limit-by = Increase today's new card limit by
custom-study-increase-todays-review-card-limit = Increase today's review card limit
custom-study-increase-todays-review-limit-by = Increase today's review limit by
-custom-study-new-cards-in-deck-over-today = New cards in deck over today limit: { $val }
custom-study-new-cards-only = New cards only
custom-study-no-cards-matched-the-criteria-you = No cards matched the criteria you provided.
custom-study-ok = OK
@@ -21,8 +20,14 @@ custom-study-review-ahead = Review ahead
custom-study-review-ahead-by = Review ahead by
custom-study-review-cards-forgotten-in-last = Review cards forgotten in last
custom-study-review-forgotten-cards = Review forgotten cards
-custom-study-reviews-due-in-deck-over-today = Reviews due in deck over today limit: { $val }
custom-study-select = Select
custom-study-select-tags-to-exclude = Select tags to exclude:
custom-study-selective-study = Selective Study
custom-study-study-by-card-state-or-tag = Study by card state or tag
+custom-study-available-new-cards = Available new cards: { $count }
+custom-study-available-review-cards = Available review cards: { $count }
+
+## DEPRECATED - you do not need to translate these.
+
+custom-study-new-cards-in-deck-over-today = New cards in deck over today limit: { $val }
+custom-study-reviews-due-in-deck-over-today = Reviews due in deck over today limit: { $val }
diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto
index ce6860d9f..c8bbf419f 100644
--- a/proto/anki/scheduler.proto
+++ b/proto/anki/scheduler.proto
@@ -40,6 +40,8 @@ service SchedulerService {
rpc StateIsLeech(SchedulingState) returns (generic.Bool);
rpc UpgradeScheduler(generic.Empty) returns (generic.Empty);
rpc CustomStudy(CustomStudyRequest) returns (collection.OpChanges);
+ rpc CustomStudyDefaults(CustomStudyDefaultsRequest)
+ returns (CustomStudyDefaultsResponse);
}
message SchedulingState {
@@ -257,17 +259,36 @@ message CustomStudyRequest {
// cards must not match any of these
repeated string tags_to_exclude = 4;
}
+ int64 deck_id = 1;
oneof value {
// increase new limit by x
- int32 new_limit_delta = 1;
+ int32 new_limit_delta = 2;
// increase review limit by x
- int32 review_limit_delta = 2;
+ int32 review_limit_delta = 3;
// repeat cards forgotten in the last x days
- uint32 forgot_days = 3;
+ uint32 forgot_days = 4;
// review cards due in the next x days
- uint32 review_ahead_days = 4;
+ uint32 review_ahead_days = 5;
// preview new cards added in the last x days
- uint32 preview_days = 5;
- Cram cram = 6;
+ uint32 preview_days = 6;
+ Cram cram = 7;
}
}
+
+message CustomStudyDefaultsRequest {
+ int64 deck_id = 1;
+}
+
+message CustomStudyDefaultsResponse {
+ message Tag {
+ string name = 1;
+ bool include = 2;
+ bool exclude = 3;
+ }
+
+ repeated Tag tags = 1;
+ uint32 extend_new = 2;
+ uint32 extend_review = 3;
+ uint32 available_new = 4;
+ uint32 available_review = 5;
+}
diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py
index 6f3c4f6c6..a5d63f829 100644
--- a/pylib/anki/scheduler/base.py
+++ b/pylib/anki/scheduler/base.py
@@ -15,6 +15,7 @@ CongratsInfo = scheduler_pb2.CongratsInfoResponse
UnburyDeck = scheduler_pb2.UnburyDeckRequest
BuryOrSuspend = scheduler_pb2.BuryOrSuspendCardsRequest
CustomStudyRequest = scheduler_pb2.CustomStudyRequest
+CustomStudyDefaults = scheduler_pb2.CustomStudyDefaultsResponse
ScheduleCardsAsNew = scheduler_pb2.ScheduleCardsAsNewRequest
ScheduleCardsAsNewDefaults = scheduler_pb2.ScheduleCardsAsNewDefaultsResponse
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate
@@ -24,7 +25,7 @@ from typing import Sequence
from anki import config_pb2
from anki.cards import CardId
-from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW, QUEUE_TYPE_REV
+from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW
from anki.decks import DeckConfigDict, DeckId, DeckTreeNode
from anki.notes import NoteId
from anki.utils import ids2str, int_time
@@ -78,21 +79,13 @@ class SchedulerBase(DeprecatedNamesMixin):
def custom_study(self, request: CustomStudyRequest) -> OpChanges:
return self.col._backend.custom_study(request)
+ def custom_study_defaults(self, deck_id: DeckId) -> CustomStudyDefaults:
+ return self.col._backend.custom_study_defaults(deck_id=deck_id)
+
def extend_limits(self, new: int, rev: int) -> None:
did = self.col.decks.current()["id"]
self.col._backend.extend_limits(deck_id=did, new_delta=new, review_delta=rev)
- # fixme: used by custom study
- def total_rev_for_current_deck(self) -> int:
- assert self.col.db
- return self.col.db.scalar(
- f"""
-select count() from cards where id in (
-select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? limit 9999)"""
- % self._deck_limit(),
- self.today,
- )
-
# fixme: only used by total_rev_for_current_deck and old deck stats;
# schedv2 defines separate version
def _deck_limit(self) -> str:
diff --git a/pylib/anki/scheduler/legacy.py b/pylib/anki/scheduler/legacy.py
index 99088ddb6..8bfee1bd7 100644
--- a/pylib/anki/scheduler/legacy.py
+++ b/pylib/anki/scheduler/legacy.py
@@ -5,8 +5,13 @@
from typing import Optional
+from anki._legacy import deprecated
from anki.cards import Card, CardId
-from anki.consts import CARD_TYPE_RELEARNING, QUEUE_TYPE_DAY_LEARN_RELEARN
+from anki.consts import (
+ CARD_TYPE_RELEARNING,
+ QUEUE_TYPE_DAY_LEARN_RELEARN,
+ QUEUE_TYPE_REV,
+)
from anki.decks import DeckConfigDict, DeckId
from anki.notes import NoteId
from anki.scheduler.base import SchedulerBase, UnburyDeck
@@ -111,6 +116,17 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
)
return from_json_bytes(self.col._backend.deck_tree_legacy())[5]
+ @deprecated(info="no longer used by Anki; will be removed in the future")
+ def total_rev_for_current_deck(self) -> int:
+ assert self.col.db
+ return self.col.db.scalar(
+ f"""
+select count() from cards where id in (
+select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? limit 9999)"""
+ % self._deck_limit(),
+ self.today,
+ )
+
# legacy in v3 but used by unit tests; redefined in v2/v1
def _cardConf(self, card: Card) -> DeckConfigDict:
diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py
index 5918e2f17..58f0a05cb 100644
--- a/pylib/anki/scheduler/v2.py
+++ b/pylib/anki/scheduler/v2.py
@@ -13,6 +13,7 @@ from typing import Any, Callable, cast
import anki # pylint: disable=unused-import
import anki.collection
from anki import hooks, scheduler_pb2
+from anki._legacy import deprecated
from anki.cards import Card, CardId
from anki.consts import *
from anki.decks import DeckConfigDict, DeckDict, DeckId
@@ -254,6 +255,7 @@ select count() from
limit = max(0, c["new"]["perDay"] - self.counts_for_deck_today(g["id"]).new)
return hooks.scheduler_new_limit_for_single_deck(limit, g)
+ @deprecated(info="no longer used by Anki; will be removed in the future")
def totalNewForCurrentDeck(self) -> int:
return self.col.db.scalar(
f"""
diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py
index af4fcaed8..10b588562 100644
--- a/pylib/anki/scheduler/v3.py
+++ b/pylib/anki/scheduler/v3.py
@@ -17,6 +17,7 @@ from __future__ import annotations
from typing import Literal, Optional, Sequence
from anki import scheduler_pb2
+from anki._legacy import deprecated
from anki.cards import Card
from anki.collection import OpChanges
from anki.consts import *
@@ -239,8 +240,7 @@ class Scheduler(SchedulerBaseWithLegacy):
except DBError:
return []
- # used by custom study; will likely be rolled into a separate routine
- # in the future
+ @deprecated(info="no longer used by Anki; will be removed in the future")
def totalNewForCurrentDeck(self) -> int:
return self.col.db.scalar(
f"""
diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py
index 0e894ec75..3f75ca793 100644
--- a/pylib/anki/tags.py
+++ b/pylib/anki/tags.py
@@ -52,19 +52,6 @@ class TagManager(DeprecatedNamesMixin):
def clear_unused_tags(self) -> OpChangesWithCount:
return self.col._backend.clear_unused_tags()
- def by_deck(self, did: DeckId, children: bool = False) -> list[str]:
- basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
- if not children:
- query = f"{basequery} AND c.did=?"
- res = self.col.db.list(query, did)
- return list(set(self.split(" ".join(res))))
- dids = [did]
- for name, id in self.col.decks.children(did):
- dids.append(id)
- query = f"{basequery} AND c.did IN {ids2str(dids)}"
- res = self.col.db.list(query)
- return list(set(self.split(" ".join(res))))
-
def set_collapsed(self, tag: str, collapsed: bool) -> OpChanges:
"Set browser expansion state for tag, registering the tag if missing."
return self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed)
@@ -181,6 +168,20 @@ class TagManager(DeprecatedNamesMixin):
def _legacy_bulk_rem(self, ids: list[NoteId], tags: str) -> None:
self._legacy_bulk_add(ids, tags, False)
+ @deprecated(info="no longer used by Anki, and will be removed in the future")
+ def by_deck(self, did: DeckId, children: bool = False) -> list[str]:
+ basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
+ if not children:
+ query = f"{basequery} AND c.did=?"
+ res = self.col.db.list(query, did)
+ return list(set(self.split(" ".join(res))))
+ dids = [did]
+ for name, id in self.col.decks.children(did):
+ dids.append(id)
+ query = f"{basequery} AND c.did IN {ids2str(dids)}"
+ res = self.col.db.list(query)
+ return list(set(self.split(" ".join(res))))
+
TagManager.register_deprecated_attributes(
registerNotes=(TagManager._legacy_register_notes, TagManager.clear_unused_tags),
diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py
index 315dbab30..1c43ca69d 100644
--- a/qt/aqt/customstudy.py
+++ b/qt/aqt/customstudy.py
@@ -1,11 +1,17 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+from typing import Tuple
+
import aqt
import aqt.forms
import aqt.operations
+from anki.collection import Collection
from anki.consts import *
+from anki.decks import DeckId
from anki.scheduler import CustomStudyRequest
+from anki.scheduler.base import CustomStudyDefaults
+from aqt.operations import QueryOp
from aqt.operations.scheduling import custom_study
from aqt.qt import *
from aqt.taglimit import TagLimit
@@ -25,17 +31,39 @@ TYPE_ALL = 3
class CustomStudy(QDialog):
- def __init__(self, mw: aqt.AnkiQt) -> None:
+ @staticmethod
+ def fetch_data_and_show(mw: aqt.AnkiQt) -> None:
+ def fetch_data(
+ col: Collection,
+ ) -> Tuple[DeckId, CustomStudyDefaults]:
+ deck_id = mw.col.decks.get_current_id()
+ defaults = col.sched.custom_study_defaults(deck_id)
+ return (deck_id, defaults)
+
+ def show_dialog(data: Tuple[DeckId, CustomStudyDefaults]) -> None:
+ deck_id, defaults = data
+ CustomStudy(mw=mw, deck_id=deck_id, defaults=defaults)
+
+ QueryOp(
+ parent=mw, op=fetch_data, success=show_dialog
+ ).with_progress().run_in_background()
+
+ def __init__(
+ self,
+ mw: aqt.AnkiQt,
+ deck_id: DeckId,
+ defaults: CustomStudyDefaults,
+ ) -> None:
+ "Don't call this directly; use CustomStudy.fetch_data_and_show()."
QDialog.__init__(self, mw)
self.mw = mw
- self.deck = self.mw.col.decks.current()
- self.conf = self.mw.col.decks.get_config(self.deck["conf"])
- self.form = f = aqt.forms.customstudy.Ui_Dialog()
- self.created_custom_study = False
- f.setupUi(self)
+ self.deck_id = deck_id
+ self.defaults = defaults
+ self.form = aqt.forms.customstudy.Ui_Dialog()
+ self.form.setupUi(self)
disable_help_button(self)
self.setupSignals()
- f.radioNew.click()
+ self.form.radioNew.click()
self.open()
def setupSignals(self) -> None:
@@ -48,82 +76,65 @@ class CustomStudy(QDialog):
qconnect(f.radioCram.clicked, lambda: self.onRadioChange(RADIO_CRAM))
def onRadioChange(self, idx: int) -> None:
- f = self.form
- sp = f.spin
- smin = 1
- smax = DYN_MAX_SIZE
- sval = 1
- post = tr.custom_study_cards()
- tit = ""
- spShow = True
- typeShow = False
+ form = self.form
+ min_spinner_value = 1
+ max_spinner_value = DYN_MAX_SIZE
+ current_spinner_value = 1
+ text_after_spinner = tr.custom_study_cards()
+ title_text = ""
+ show_cram_type = False
ok = tr.custom_study_ok()
- def plus(num: Union[int, str]) -> str:
- if num == 1000:
- num = "1000+"
- return f"{str(num)}"
-
if idx == RADIO_NEW:
- new = self.mw.col.sched.totalNewForCurrentDeck()
- # get the number of new cards in deck that exceed the new cards limit
- newUnderLearning = min(
- new, self.conf["new"]["perDay"] - self.deck["newToday"][1]
+ title_text = tr.custom_study_available_new_cards(
+ count=self.defaults.available_new
)
- newExceeding = min(new, new - newUnderLearning)
- tit = tr.custom_study_new_cards_in_deck_over_today(val=plus(newExceeding))
- pre = tr.custom_study_increase_todays_new_card_limit_by()
- sval = min(new, self.deck.get("extendNew", 10))
- smin = -DYN_MAX_SIZE
- smax = newExceeding
+ text_before_spinner = tr.custom_study_increase_todays_new_card_limit_by()
+ current_spinner_value = self.defaults.extend_new
+ min_spinner_value = -DYN_MAX_SIZE
elif idx == RADIO_REV:
- rev = self.mw.col.sched.total_rev_for_current_deck()
- # get the number of review due in deck that exceed the review due limit
- revUnderLearning = min(
- rev, self.conf["rev"]["perDay"] - self.deck["revToday"][1]
+ title_text = tr.custom_study_available_review_cards(
+ count=self.defaults.available_review
)
- revExceeding = min(rev, rev - revUnderLearning)
- tit = tr.custom_study_reviews_due_in_deck_over_today(val=plus(revExceeding))
- pre = tr.custom_study_increase_todays_review_limit_by()
- sval = min(rev, self.deck.get("extendRev", 10))
- smin = -DYN_MAX_SIZE
- smax = revExceeding
+ text_before_spinner = tr.custom_study_increase_todays_review_limit_by()
+ current_spinner_value = self.defaults.extend_review
+ min_spinner_value = -DYN_MAX_SIZE
elif idx == RADIO_FORGOT:
- pre = tr.custom_study_review_cards_forgotten_in_last()
- post = tr.scheduling_days()
- smax = 30
+ text_before_spinner = tr.custom_study_review_cards_forgotten_in_last()
+ text_after_spinner = tr.scheduling_days()
+ max_spinner_value = 30
elif idx == RADIO_AHEAD:
- pre = tr.custom_study_review_ahead_by()
- post = tr.scheduling_days()
+ text_before_spinner = tr.custom_study_review_ahead_by()
+ text_after_spinner = tr.scheduling_days()
elif idx == RADIO_PREVIEW:
- pre = tr.custom_study_preview_new_cards_added_in_the()
- post = tr.scheduling_days()
- sval = 1
+ text_before_spinner = tr.custom_study_preview_new_cards_added_in_the()
+ text_after_spinner = tr.scheduling_days()
+ current_spinner_value = 1
elif idx == RADIO_CRAM:
- pre = tr.custom_study_select()
- post = tr.custom_study_cards_from_the_deck()
- # tit = _("After pressing OK, you can choose which tags to include.")
+ text_before_spinner = tr.custom_study_select()
+ text_after_spinner = tr.custom_study_cards_from_the_deck()
ok = tr.custom_study_choose_tags()
- sval = 100
- typeShow = True
- sp.setVisible(spShow)
- f.cardType.setVisible(typeShow)
- f.title.setText(tit)
- f.title.setVisible(not not tit)
- f.spin.setMinimum(smin)
- f.spin.setMaximum(smax)
- if smax > 0:
- f.spin.setEnabled(True)
+ current_spinner_value = 100
+ show_cram_type = True
+
+ form.spin.setVisible(True)
+ form.cardType.setVisible(show_cram_type)
+ form.title.setText(title_text)
+ form.title.setVisible(not not title_text)
+ form.spin.setMinimum(min_spinner_value)
+ form.spin.setMaximum(max_spinner_value)
+ if max_spinner_value > 0:
+ form.spin.setEnabled(True)
else:
- f.spin.setEnabled(False)
- f.spin.setValue(sval)
- f.preSpin.setText(pre)
- f.postSpin.setText(post)
- f.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(ok)
+ form.spin.setEnabled(False)
+ form.spin.setValue(current_spinner_value)
+ form.preSpin.setText(text_before_spinner)
+ form.postSpin.setText(text_after_spinner)
+ form.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(ok)
self.radioIdx = idx
def accept(self) -> None:
- request = CustomStudyRequest()
+ request = CustomStudyRequest(deck_id=self.deck_id)
if self.radioIdx == RADIO_NEW:
request.new_limit_delta = self.form.spin.value()
elif self.radioIdx == RADIO_REV:
@@ -137,10 +148,6 @@ class CustomStudy(QDialog):
else:
request.cram.card_limit = self.form.spin.value()
- tags = TagLimit.get_tags(self.mw, self)
- request.cram.tags_to_include.extend(tags[0])
- request.cram.tags_to_exclude.extend(tags[1])
-
cram_type = self.form.cardType.currentRow()
if cram_type == TYPE_NEW:
request.cram.kind = CustomStudyRequest.Cram.CRAM_KIND_NEW
@@ -151,15 +158,21 @@ class CustomStudy(QDialog):
else:
request.cram.kind = CustomStudyRequest.Cram.CRAM_KIND_ALL
+ def on_done(include: list[str], exclude: list[str]) -> None:
+ request.cram.tags_to_include.extend(include)
+ request.cram.tags_to_exclude.extend(exclude)
+ self._create_and_close(request)
+
+ # continues in background
+ TagLimit(self, self.defaults.tags, on_done)
+ return
+
+ # other cases are synchronous
+ self._create_and_close(request)
+
+ def _create_and_close(self, request: CustomStudyRequest) -> None:
# keep open on failure, as the cause was most likely an empty search
# result, which the user can remedy
custom_study(parent=self, request=request).success(
lambda _: QDialog.accept(self)
).run_in_background()
-
- def reject(self) -> None:
- if self.created_custom_study:
- # set the original deck back to current
- self.mw.col.decks.select(self.deck["id"])
- # fixme: clean up the empty custom study deck
- QDialog.reject(self)
diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py
index 59b51a28f..442af6d68 100644
--- a/qt/aqt/overview.py
+++ b/qt/aqt/overview.py
@@ -304,4 +304,4 @@ class Overview:
def onStudyMore(self) -> None:
import aqt.customstudy
- aqt.customstudy.CustomStudy(self.mw)
+ aqt.customstudy.CustomStudy.fetch_data_and_show(self.mw)
diff --git a/qt/aqt/taglimit.py b/qt/aqt/taglimit.py
index ef87bb173..5cc172a66 100644
--- a/qt/aqt/taglimit.py
+++ b/qt/aqt/taglimit.py
@@ -3,117 +3,93 @@
from __future__ import annotations
-from typing import List, Optional, Tuple
+from typing import Sequence
import aqt
import aqt.customstudy
import aqt.forms
from anki.lang import with_collapsed_whitespace
-from aqt.main import AnkiQt
+from anki.scheduler.base import CustomStudyDefaults
from aqt.qt import *
from aqt.utils import disable_help_button, restoreGeom, saveGeom, showWarning, tr
class TagLimit(QDialog):
- @staticmethod
- def get_tags(
- mw: AnkiQt, parent: aqt.customstudy.CustomStudy
- ) -> Tuple[List[str], List[str]]:
- """Get two lists of tags to include/exclude."""
- return TagLimit(mw, parent).tags
-
- def __init__(self, mw: AnkiQt, parent: aqt.customstudy.CustomStudy) -> None:
+ def __init__(
+ self,
+ parent: QWidget,
+ tags: Sequence[CustomStudyDefaults.Tag],
+ on_success: Callable[[list[str], list[str]], None],
+ ) -> None:
+ "Ask user to select tags. on_success() will be called with selected included and excluded tags."
QDialog.__init__(self, parent, Qt.WindowType.Window)
- self.tags: Tuple[List[str], List[str]] = ([], [])
- self.tags_list: list[str] = []
- self.mw = mw
- self.parent_: Optional[aqt.customstudy.CustomStudy] = parent
- self.deck = self.parent_.deck
- self.dialog = aqt.forms.taglimit.Ui_Dialog()
- self.dialog.setupUi(self)
+ self.tags = tags
+ self.form = aqt.forms.taglimit.Ui_Dialog()
+ self.form.setupUi(self)
+ self.on_success = on_success
disable_help_button(self)
s = QShortcut(
QKeySequence("ctrl+d"),
- self.dialog.activeList,
+ self.form.activeList,
context=Qt.ShortcutContext.WidgetShortcut,
)
- qconnect(s.activated, self.dialog.activeList.clearSelection)
+ qconnect(s.activated, self.form.activeList.clearSelection)
s = QShortcut(
QKeySequence("ctrl+d"),
- self.dialog.inactiveList,
+ self.form.inactiveList,
context=Qt.ShortcutContext.WidgetShortcut,
)
- qconnect(s.activated, self.dialog.inactiveList.clearSelection)
- self.rebuildTagList()
+ qconnect(s.activated, self.form.inactiveList.clearSelection)
+ self.build_tag_lists()
restoreGeom(self, "tagLimit")
- self.exec()
+ self.open()
- def rebuildTagList(self) -> None:
- usertags = self.mw.col.tags.by_deck(self.deck["id"], True)
- yes = self.deck.get("activeTags", [])
- no = self.deck.get("inactiveTags", [])
- yesHash = {}
- noHash = {}
- for y in yes:
- yesHash[y] = True
- for n in no:
- noHash[n] = True
- groupedTags = []
- usertags.sort()
- groupedTags.append(usertags)
- self.tags_list = []
- for tags in groupedTags:
- for t in tags:
- self.tags_list.append(t)
- item = QListWidgetItem(t.replace("_", " "))
- self.dialog.activeList.addItem(item)
- if t in yesHash:
- mode = QItemSelectionModel.SelectionFlag.Select
- self.dialog.activeCheck.setChecked(True)
- else:
- mode = QItemSelectionModel.SelectionFlag.Deselect
- idx = self.dialog.activeList.indexFromItem(item)
- self.dialog.activeList.selectionModel().select(idx, mode)
- # inactive
- item = QListWidgetItem(t.replace("_", " "))
- self.dialog.inactiveList.addItem(item)
- if t in noHash:
- mode = QItemSelectionModel.SelectionFlag.Select
- else:
- mode = QItemSelectionModel.SelectionFlag.Deselect
- idx = self.dialog.inactiveList.indexFromItem(item)
- self.dialog.inactiveList.selectionModel().select(idx, mode)
+ def build_tag_lists(self) -> None:
+ def add_tag(tag: str, select: bool, list: QListWidget) -> None:
+ item = QListWidgetItem(tag.replace("_", " "))
+ list.addItem(item)
+ if select:
+ idx = list.indexFromItem(item)
+ list.selectionModel().select(
+ idx, QItemSelectionModel.SelectionFlag.Select
+ )
+
+ had_included_tag = False
+
+ for tag in self.tags:
+ if tag.include:
+ had_included_tag = True
+ add_tag(tag.name, tag.include, self.form.activeList)
+ add_tag(tag.name, tag.exclude, self.form.inactiveList)
+
+ if had_included_tag:
+ self.form.activeCheck.setChecked(True)
def reject(self) -> None:
QDialog.reject(self)
def accept(self) -> None:
- include_tags = exclude_tags = []
- # gather yes/no tags
- for c in range(self.dialog.activeList.count()):
+ include_tags = []
+ exclude_tags = []
+ want_active = self.form.activeCheck.isChecked()
+ for c, tag in enumerate(self.tags):
# active
- if self.dialog.activeCheck.isChecked():
- item = self.dialog.activeList.item(c)
- idx = self.dialog.activeList.indexFromItem(item)
- if self.dialog.activeList.selectionModel().isSelected(idx):
- include_tags.append(self.tags_list[c])
+ if want_active:
+ item = self.form.activeList.item(c)
+ idx = self.form.activeList.indexFromItem(item)
+ if self.form.activeList.selectionModel().isSelected(idx):
+ include_tags.append(tag.name)
# inactive
- item = self.dialog.inactiveList.item(c)
- idx = self.dialog.inactiveList.indexFromItem(item)
- if self.dialog.inactiveList.selectionModel().isSelected(idx):
- exclude_tags.append(self.tags_list[c])
+ item = self.form.inactiveList.item(c)
+ idx = self.form.inactiveList.indexFromItem(item)
+ if self.form.inactiveList.selectionModel().isSelected(idx):
+ exclude_tags.append(tag.name)
if (len(include_tags) + len(exclude_tags)) > 100:
showWarning(with_collapsed_whitespace(tr.errors_100_tags_max()))
return
- self.hide()
- self.tags = (include_tags, exclude_tags)
-
- # save in the deck for future invocations
- self.deck["activeTags"] = include_tags
- self.deck["inactiveTags"] = exclude_tags
- self.mw.col.decks.save(self.deck)
-
saveGeom(self, "tagLimit")
QDialog.accept(self)
+
+ self.on_success(include_tags, exclude_tags)
diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs
index 341431f3b..91024fff2 100644
--- a/rslib/src/backend/scheduler/mod.rs
+++ b/rslib/src/backend/scheduler/mod.rs
@@ -201,6 +201,13 @@ impl SchedulerService for Backend {
fn custom_study(&self, input: pb::CustomStudyRequest) -> Result {
self.with_col(|col| col.custom_study(input)).map(Into::into)
}
+
+ fn custom_study_defaults(
+ &self,
+ input: pb::CustomStudyDefaultsRequest,
+ ) -> Result {
+ self.with_col(|col| col.custom_study_defaults(input.deck_id.into()))
+ }
}
impl From for pb::SchedTimingTodayResponse {
diff --git a/rslib/src/config/deck.rs b/rslib/src/config/deck.rs
index 8c5a7e153..d78d7d387 100644
--- a/rslib/src/config/deck.rs
+++ b/rslib/src/config/deck.rs
@@ -8,12 +8,14 @@ use crate::prelude::*;
/// Auxillary deck state, stored in the config table.
#[derive(Debug, Clone, Copy, IntoStaticStr)]
#[strum(serialize_all = "camelCase")]
-enum DeckConfigKey {
+pub enum DeckConfigKey {
LastNotetype,
+ CustomStudyIncludeTags,
+ CustomStudyExcludeTags,
}
impl DeckConfigKey {
- fn for_deck(self, did: DeckId) -> String {
+ pub fn for_deck(self, did: DeckId) -> String {
build_aux_deck_key(did, <&'static str>::from(self))
}
}
diff --git a/rslib/src/config/mod.rs b/rslib/src/config/mod.rs
index 955d65832..c4c8ccc4c 100644
--- a/rslib/src/config/mod.rs
+++ b/rslib/src/config/mod.rs
@@ -13,7 +13,9 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
use slog::warn;
use strum::IntoStaticStr;
-pub use self::{bool::BoolKey, notetype::get_aux_notetype_config_key, string::StringKey};
+pub use self::{
+ bool::BoolKey, deck::DeckConfigKey, notetype::get_aux_notetype_config_key, string::StringKey,
+};
use crate::{backend_proto::preferences::Backups, prelude::*};
/// Only used when updating/undoing.
@@ -112,10 +114,10 @@ impl Collection {
}
// /// Get config item, returning default value if missing/invalid.
- pub(crate) fn get_config_default(&self, key: K) -> T
+ pub(crate) fn get_config_default<'a, T, K>(&self, key: K) -> T
where
T: DeserializeOwned + Default,
- K: Into<&'static str>,
+ K: Into<&'a str>,
{
self.get_config_optional(key).unwrap_or_default()
}
diff --git a/rslib/src/decks/tree.rs b/rslib/src/decks/tree.rs
index 9979c598b..e96da4267 100644
--- a/rslib/src/decks/tree.rs
+++ b/rslib/src/decks/tree.rs
@@ -244,17 +244,28 @@ fn hide_default_deck(node: &mut DeckTreeNode) {
}
}
-fn get_subnode(top: DeckTreeNode, target: DeckId) -> Option {
- if top.deck_id == target.0 {
- return Some(top);
- }
- for child in top.children {
- if let Some(node) = get_subnode(child, target) {
- return Some(node);
+impl DeckTreeNode {
+ /// Locate provided deck in tree, and return it.
+ pub fn get_deck(self, deck_id: DeckId) -> Option {
+ if self.deck_id == deck_id.0 {
+ return Some(self);
}
+ for child in self.children {
+ if let Some(node) = child.get_deck(deck_id) {
+ return Some(node);
+ }
+ }
+
+ None
}
- None
+ pub(crate) fn sum(&self, map: fn(&DeckTreeNode) -> T) -> T {
+ let mut output = map(self);
+ for child in &self.children {
+ output += child.sum(map);
+ }
+ output
+ }
}
#[derive(Serialize_tuple)]
@@ -330,7 +341,7 @@ impl Collection {
pub fn current_deck_tree(&mut self) -> Result