From 44d9dcc7dccd1922db444bcbb4c0470625b5e8fa Mon Sep 17 00:00:00 2001 From: Mateus Etto Date: Thu, 25 Apr 2024 13:31:04 +0900 Subject: [PATCH 1/2] Allow to full recover slowly --- src/database.py | 2 +- src/deck_manager.py | 21 +++++++++++++++--- src/defaults.py | 1 + src/lifedrain.py | 53 +++++++++++++++++++++++---------------------- src/main.py | 7 +++--- src/progress_bar.py | 34 +++++++++++++---------------- src/settings.py | 16 +++++++++++++- 7 files changed, 80 insertions(+), 54 deletions(-) diff --git a/src/database.py b/src/database.py index 911a836..2bb42d2 100644 --- a/src/database.py +++ b/src/database.py @@ -61,7 +61,7 @@ def update(self, new_conf: dict[str, Any]) -> None: class DeckConf: """Manages Life Drain's deck configuration.""" FIELDS: ClassVar[set[str]] = { - 'enable', 'maxLife', 'recover', 'damage', 'damageNew', 'damageLearning', + 'enable', 'maxLife', 'recover', 'damage', 'damageNew', 'damageLearning', 'fullRecoverSpeed', } def __init__(self, mw: AnkiQt): diff --git a/src/deck_manager.py b/src/deck_manager.py index ad9fab4..51006cd 100644 --- a/src/deck_manager.py +++ b/src/deck_manager.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Union from anki.hooks import runHook +from aqt.progress import ProgressManager from .decorators import must_have_active_deck from .defaults import BEHAVIORS @@ -34,6 +35,9 @@ def __init__(self, mw: AnkiQt, qt: Any, global_conf: GlobalConf, deck_conf: Deck global_conf: An instance of GlobalConf. deck_conf: An instance of DeckConf. """ + self.recovering: bool = False + self.timer = ProgressManager(mw).timer(100, self.life_timer, repeat=True, parent=mw) + self.timer.stop() self._progress_bar = ProgressBar(mw, qt) self._global_conf = global_conf self._deck_conf = deck_conf @@ -82,6 +86,7 @@ def set_deck_conf(self, conf: dict[str, Any], *, update_life: bool) -> None: bar_info['enable'] = conf['enable'] bar_info['maxValue'] = conf['maxLife'] bar_info['recoverValue'] = conf['recover'] + bar_info['fullRecoverSpeed'] = conf['fullRecoverSpeed'] bar_info['damageValue'] = conf['damage'] bar_info['damageNew'] = conf['damageNew'] bar_info['damageLearning'] = conf['damageLearning'] @@ -93,9 +98,18 @@ def set_deck_conf(self, conf: dict[str, Any], *, update_life: bool) -> None: bar_info['currentValue'] = current_value @must_have_active_deck - def drain(self, bar_info: dict[str, Any]) -> None: - """Life loss due to drain.""" - self._update_life(bar_info, -0.1) + def life_timer(self, bar_info: dict[str, Any]) -> None: + """Life loss due to drain, or life gained due to recover.""" + if self.recovering: + if bar_info['fullRecoverSpeed'] == 0: + self.recover() + else: + self._update_life(bar_info, bar_info['fullRecoverSpeed'] / 10) + else: + self._update_life(bar_info, -0.1) # Drain + + if bar_info['currentValue'] in [0, bar_info['maxValue']]: + self.timer.stop() @must_have_active_deck def heal(self, bar_info: dict[str, Any], value:Optional[Union[int, float]]=None, *, @@ -231,6 +245,7 @@ def _add_deck(self, deck_id:str) -> None: 'enable': conf['enable'], 'maxValue': conf['maxLife'], 'recoverValue': conf['recover'], + 'fullRecoverSpeed': conf['fullRecoverSpeed'], 'damageValue': conf['damage'], 'damageNew': conf['damageNew'], 'damageLearning': conf['damageLearning'], diff --git a/src/defaults.py b/src/defaults.py index e25d32d..4f6ba1e 100644 --- a/src/defaults.py +++ b/src/defaults.py @@ -29,6 +29,7 @@ DEFAULTS = { 'maxLife': 120, 'recover': 5, + 'fullRecoverSpeed': 0, 'damage': None, 'damageNew': None, 'damageLearning': None, diff --git a/src/lifedrain.py b/src/lifedrain.py index dee4cb5..4a6e521 100644 --- a/src/lifedrain.py +++ b/src/lifedrain.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Union +from typing import TYPE_CHECKING, Any, Union from . import settings from .database import DeckConf, GlobalConf @@ -27,18 +27,15 @@ class Lifedrain: status: A dictionary that keeps track the events on Anki. """ - def __init__(self, make_timer: Callable, mw: AnkiQt, qt: Any): + def __init__(self, mw: AnkiQt, qt: Any): """Initializes DeckManager and Settings, and add-on initial setup. Args: - make_timer: A function that creates a timer. mw: Anki's main window. qt: The PyQt library. """ self._qt = qt self._mw = mw - self._timer = make_timer(100, lambda: self.deck_manager.drain(), repeat=True, parent=mw) - self._timer.stop() self.config = GlobalConf(mw) self._deck_config = DeckConf(mw) self.deck_manager = DeckManager(mw, qt, self.config, self._deck_config) @@ -53,8 +50,8 @@ def __init__(self, make_timer: Callable, mw: AnkiQt, qt: Any): def global_settings(self) -> None: """Opens a dialog with the Global Settings.""" - drain_enabled = self._timer.isActive() - self._toggle_drain(enable=False) + drain_enabled = self.deck_manager.timer.isActive() + self.toggle_drain(enable=False) settings.global_settings( aqt=self._qt, mw=self._mw, @@ -64,7 +61,7 @@ def global_settings(self) -> None: config = self.config.get() if config['enable']: self.update_global_shortcuts() - self._toggle_drain(drain_enabled) + self.toggle_drain(drain_enabled) self.deck_manager.update(self.status['screen']) else: self.update_global_shortcuts() @@ -72,8 +69,8 @@ def global_settings(self) -> None: def deck_settings(self) -> None: """Opens a dialog with the Deck Settings.""" - drain_enabled = self._timer.isActive() - self._toggle_drain(enable=False) + drain_enabled = self.deck_manager.timer.isActive() + self.toggle_drain(enable=False) settings.deck_settings( aqt=self._qt, mw=self._mw, @@ -81,7 +78,7 @@ def deck_settings(self) -> None: global_config=self.config, deck_manager=self.deck_manager, ) - self._toggle_drain(drain_enabled) + self.toggle_drain(drain_enabled) self.deck_manager.update(self.status['screen']) def update_global_shortcuts(self) -> None: @@ -102,7 +99,7 @@ def review_shortcuts(self, shortcuts: list[tuple]) -> None: if config['deckSettingsShortcut']: shortcuts.append((config['deckSettingsShortcut'], self.deck_settings)) if config['enable'] and config['pauseShortcut']: - shortcuts.append((config['pauseShortcut'], self._toggle_drain)) + shortcuts.append((config['pauseShortcut'], self.toggle_drain)) def overview_shortcuts(self, shortcuts: list[tuple]) -> None: """Generates the overview screen shortcuts.""" @@ -110,9 +107,10 @@ def overview_shortcuts(self, shortcuts: list[tuple]) -> None: if config['deckSettingsShortcut']: shortcuts.append((config['deckSettingsShortcut'], self.deck_settings)) if config['enable'] and config['recoverShortcut']: - def full_recover() -> None: - self.deck_manager.recover() - shortcuts.append((config['recoverShortcut'], full_recover)) + def start_recover() -> None: + self.deck_manager.recovering = True + self.toggle_drain() + shortcuts.append((config['recoverShortcut'], start_recover)) def screen_change(self, state: MainWindowState) -> None: """Updates Life Drain when the screen changes. @@ -130,25 +128,27 @@ def screen_change(self, state: MainWindowState) -> None: self.deck_manager.update(state) if state != 'review': - self._toggle_drain(enable=False) + self.toggle_drain(enable=False) self.status['prev_card'] = None - if self.status['reviewed'] and state in ['overview', 'review']: + if state != 'overview': + self.deck_manager.recovering = False + if state != 'deckBrowser' and self.status['reviewed']: self.deck_manager.answer( self.status['review_response'], self.status['card_type'], ) - self.status['reviewed'] = False + self.status['reviewed'] = False @must_be_enabled def opened_window(self, config: dict[str, Any]) -> None: """Called when a window is opened while reviewing.""" if config['stopOnLostFocus']: - self._toggle_drain(enable=False) + self.toggle_drain(enable=False) @must_be_enabled def show_question(self, config: dict[str, Any], card: Card) -> None: """Called when a question is shown.""" - self._toggle_drain(enable=True) + self.toggle_drain(enable=True) if self.status['action'] == 'undo': self.deck_manager.undo() elif self.status['action'] == 'bury': @@ -169,18 +169,19 @@ def show_question(self, config: dict[str, Any], card: Card) -> None: @must_be_enabled def show_answer(self, config: dict[str, Any]) -> None: """Called when an answer is shown.""" - self._toggle_drain(not config['stopOnAnswer']) + self.toggle_drain(not config['stopOnAnswer']) self.status['reviewed'] = True @must_be_enabled - def _toggle_drain(self, config: dict[str, Any], enable: Union[bool, None]=None) -> None: # noqa: ARG002 + def toggle_drain(self, config: dict[str, Any], enable: Union[bool, None]=None) -> None: # noqa: ARG002 """Toggles the life drain. Args: config: Global configuration dictionary. enable: Optional. Enables the drain if True. """ - if self._timer.isActive() and enable is not True: - self._timer.stop() - elif not self._timer.isActive() and enable is not False: - self._timer.start() + is_active = self.deck_manager.timer.isActive() + if is_active and enable is not True: + self.deck_manager.timer.stop() + elif not is_active and enable is not False: + self.deck_manager.timer.start() diff --git a/src/main.py b/src/main.py index ed50204..d74bda4 100644 --- a/src/main.py +++ b/src/main.py @@ -8,7 +8,6 @@ from anki import hooks from anki.decks import DeckId from aqt import gui_hooks, mw, qt -from aqt.progress import ProgressManager from .defaults import DEFAULTS from .exceptions import GetCollectionError, GetMainWindowError @@ -20,8 +19,7 @@ def main() -> None: if mw is None: raise GetMainWindowError - make_timer = ProgressManager(mw).timer - lifedrain = Lifedrain(make_timer, mw, qt) + lifedrain = Lifedrain(mw, qt) setup_shortcuts(lifedrain) setup_state_change(lifedrain) @@ -82,7 +80,8 @@ def custom_link_handler(url: str) -> bool: if url == 'lifedrain': lifedrain.deck_settings() elif url == 'recover': - lifedrain.deck_manager.recover() + lifedrain.deck_manager.recovering = True + lifedrain.toggle_drain() return link_handler(url=url) return custom_link_handler diff --git a/src/progress_bar.py b/src/progress_bar.py index f17462b..fa6c5a4 100644 --- a/src/progress_bar.py +++ b/src/progress_bar.py @@ -3,7 +3,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal, Union +import math +from typing import TYPE_CHECKING, Any, Literal from .defaults import POSITION_OPTIONS, STYLE_OPTIONS, TEXT_FORMAT @@ -28,7 +29,7 @@ def __init__(self, mw: AnkiQt, qt: Any): self._mw = mw self._qt = qt self._qprogressbar = qt.QProgressBar() - self._current_value: Union[int, float] = 1 + self._current_value: float = 1 self._dock: dict[str, Any] = {} self._max_value: float = 1 self._text_format: str = '' @@ -56,10 +57,8 @@ def set_max_value(self, max_value: float) -> None: Args: max_value: The maximum value of the bar. Up to 1 decimal place. """ - self._max_value = max_value * 10 - if self._max_value <= 0: - self._max_value = 1 - self._qprogressbar.setRange(0, self._max_value) + self._max_value = max(1, max_value) + self._qprogressbar.setRange(0, self._max_value * 10) def set_current_value(self, current_value: float) -> None: """Sets the current value for the bar. @@ -67,7 +66,7 @@ def set_current_value(self, current_value: float) -> None: Args: current_value: The current value of the bar. Up to 1 decimal place. """ - self._current_value = int(current_value * 10) + self._current_value = current_value self._validate_current_value() self._update_text() self._update_bar_color() @@ -78,15 +77,14 @@ def inc_current_value(self, increment: float) -> None: Args: increment: A positive or negative number. Up to 1 decimal place. """ - self._current_value += int(increment * 10) + self._current_value += increment self._validate_current_value() - if self._current_value % 10 == 0 or abs(increment) >= 1: - self._update_text() - self._update_bar_color() + self._update_text() + self._update_bar_color() def get_current_value(self) -> float: """Gets the current value of the bar.""" - return float(self._current_value) / 10 + return self._current_value def set_style(self, options: dict[str, Any]) -> None: """Sets the styling of the Progress Bar. @@ -152,7 +150,7 @@ def _validate_current_value(self) -> None: self._current_value = self._max_value elif self._current_value < 0: self._current_value = 0 - self._qprogressbar.setValue(self._current_value) + self._qprogressbar.setValue(int(self._current_value * 10)) self._qprogressbar.update() def _update_text(self) -> None: @@ -160,14 +158,12 @@ def _update_text(self) -> None: if not self._text_format: return if self._text_format == 'mm:ss': - minutes = int(self._current_value / 600) - seconds = int((self._current_value / 10) % 60) + minutes = int(self._current_value / 60) + seconds = int(self._current_value) % 60 self._qprogressbar.setFormat(f'{minutes:01d}:{seconds:02d}') else: - current_value = int(self._current_value / 10) - if self._current_value % 10 != 0: - current_value += 1 - max_value = int(self._max_value / 10) + current_value = math.ceil(self._current_value) + max_value = int(self._max_value) text = self._text_format text = text.replace('%v', str(current_value)) text = text.replace('%m', str(max_value)) diff --git a/src/settings.py b/src/settings.py index 7bba8c2..3c9b8ee 100644 --- a/src/settings.py +++ b/src/settings.py @@ -267,6 +267,7 @@ def save() -> None: 'shareDrain': deck_defaults_tab.shareDrain.get_value(), 'maxLife': deck_defaults_tab.maxLifeInput.value(), 'recover': deck_defaults_tab.recoverInput.value(), + 'fullRecoverSpeed': deck_defaults_tab.fullRecoverInput.value(), 'damage': damage, 'damageNew': damage_new, 'damageLearning': damage_learning, @@ -433,6 +434,11 @@ def generate_form() -> Any: seconds for the life bar go from full to empty.''') tab.spin_box('recoverInput', 'Recover', [0, 1000], '''Time in seconds \ that is recovered after answering a card.''') + tab.double_spin_box('fullRecoverInput', 'Full recover speed', [-10000, 10000], '''Amount \ +to recover each second when clicking the "Recover" button in the deck overview screen. Negative \ +values allowed. +Use 0 for the default behaviour: instant recovery. +The recover will stop once life reaches 0, maximum, or when leaving deck overview screen.''') tab.check_box('enableDamageInput', 'Enable damage', "Enable the damage feature. It will be triggered when \ answering with 'Again'.") @@ -449,6 +455,7 @@ def load_data(widget: Any, conf: dict[str, Any]) -> None: widget.shareDrain.set_value(conf['shareDrain']) widget.maxLifeInput.set_value(conf['maxLife']) widget.recoverInput.set_value(conf['recover']) + widget.fullRecoverInput.set_value(conf['fullRecoverSpeed']) def update_damageinput() -> None: damage_enabled = widget.enableDamageInput.isChecked() @@ -538,6 +545,7 @@ def save() -> None: 'enable': basic_tab.enable.isChecked(), 'maxLife': basic_tab.maxLifeInput.value(), 'recover': basic_tab.recoverInput.value(), + 'fullRecoverSpeed': basic_tab.fullRecoverInput.value(), 'damage': damage, 'damageNew': damage_new, 'damageLearning': damage_learning, @@ -598,10 +606,15 @@ def generate_form() -> Any: 'Enable/disable Life Drain for this deck.') tab.spin_box('maxLifeInput', 'Maximum life', [1, 10000], '''Time in \ seconds for the life bar go from full to empty.''') - tab.spin_box('recoverInput', 'Recover', [0, 1000], '''Time in seconds \ + tab.spin_box('recoverInput', 'Answer recover', [0, 1000], '''Time in seconds \ that is recovered after answering a card.''') tab.double_spin_box('currentValueInput', 'Current life', [0, 10000], 'Current life, in seconds.') + tab.double_spin_box('fullRecoverInput', 'Full recover speed', [-10000, 10000], '''Amount \ +to recover each second when clicking the "Recover" button in the deck overview screen. Negative \ +values allowed. +Use 0 for the default behaviour: instant recovery. +The recover will stop once life reaches 0, maximum, or when leaving deck overview screen.''') tab.fill_space() return tab.widget @@ -610,6 +623,7 @@ def load_data(widget: Any, conf: dict[str, Any]) -> None: widget.maxLifeInput.set_value(conf['maxLife']) widget.recoverInput.set_value(conf['recover']) widget.currentValueInput.set_value(life) + widget.fullRecoverInput.set_value(conf['fullRecoverSpeed']) tab = generate_form() load_data(tab, conf) From b88774a610b94ae848ec7f92950fdc88e26ac8ef Mon Sep 17 00:00:00 2001 From: Mateus Etto Date: Thu, 25 Apr 2024 13:41:01 +0900 Subject: [PATCH 2/2] Behavior --- src/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/settings.py b/src/settings.py index 3c9b8ee..56c5d86 100644 --- a/src/settings.py +++ b/src/settings.py @@ -437,7 +437,7 @@ def generate_form() -> Any: tab.double_spin_box('fullRecoverInput', 'Full recover speed', [-10000, 10000], '''Amount \ to recover each second when clicking the "Recover" button in the deck overview screen. Negative \ values allowed. -Use 0 for the default behaviour: instant recovery. +Use 0 for the default behavior: instant recovery. The recover will stop once life reaches 0, maximum, or when leaving deck overview screen.''') tab.check_box('enableDamageInput', 'Enable damage', "Enable the damage feature. It will be triggered when \ @@ -613,7 +613,7 @@ def generate_form() -> Any: tab.double_spin_box('fullRecoverInput', 'Full recover speed', [-10000, 10000], '''Amount \ to recover each second when clicking the "Recover" button in the deck overview screen. Negative \ values allowed. -Use 0 for the default behaviour: instant recovery. +Use 0 for the default behavior: instant recovery. The recover will stop once life reaches 0, maximum, or when leaving deck overview screen.''') tab.fill_space() return tab.widget