2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2012-12-21 08:51:59 +01:00
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
2020-02-08 23:59:29 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2020-08-16 18:33:33 +02:00
|
|
|
import enum
|
2019-12-20 10:19:03 +01:00
|
|
|
import gc
|
2020-01-03 17:57:33 +01:00
|
|
|
import os
|
2013-10-18 00:48:45 +02:00
|
|
|
import re
|
|
|
|
import signal
|
2020-04-25 11:44:48 +02:00
|
|
|
import weakref
|
2019-12-20 10:19:03 +01:00
|
|
|
from argparse import Namespace
|
2021-02-02 14:30:53 +01:00
|
|
|
from concurrent.futures import Future
|
2021-10-03 10:59:42 +02:00
|
|
|
from typing import Any, Literal, Sequence, TextIO, TypeVar, cast
|
2019-12-20 10:19:03 +01:00
|
|
|
|
2020-01-24 06:48:40 +01:00
|
|
|
import anki
|
2022-02-13 04:40:47 +01:00
|
|
|
import anki.cards
|
|
|
|
import anki.sound
|
2013-10-18 00:48:45 +02:00
|
|
|
import aqt
|
2022-02-13 04:40:47 +01:00
|
|
|
import aqt.forms
|
2019-12-20 10:19:03 +01:00
|
|
|
import aqt.mediasrv
|
2020-01-02 10:43:19 +01:00
|
|
|
import aqt.mpv
|
2022-02-13 04:40:47 +01:00
|
|
|
import aqt.operations
|
2013-10-18 00:48:45 +02:00
|
|
|
import aqt.progress
|
2020-01-02 10:43:19 +01:00
|
|
|
import aqt.sound
|
2013-10-18 00:48:45 +02:00
|
|
|
import aqt.stats
|
2019-12-20 10:19:03 +01:00
|
|
|
import aqt.toolbar
|
|
|
|
import aqt.webview
|
Backup improvements (#1728)
* Collection needs to be closed prior to backup even when not downgrading
* Backups -> BackupLimits
* Some improvements to backup_task
- backup_inner now returns the error instead of logging it, so that
the frontend can discover the issue when they await a backup (or create
another one)
- start_backup() was acquiring backup_task twice, and if another thread
started a backup between the two locks, the task could have been accidentally
overwritten without awaiting it
* Backups no longer require a collection close
- Instead of closing the collection, we ensure there is no active
transaction, and flush the WAL to disk. This means the undo history
is no longer lost on backup, which will be particularly useful if we
add a periodic backup in the future.
- Because a close is no longer required, backups are now achieved with
a separate command, instead of being included in CloseCollection().
- Full sync no longer requires an extra close+reopen step, and we now
wait for the backup to complete before proceeding.
- Create a backup before 'check db'
* Add File>Create Backup
https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157
* Defer checkpoint until we know we need it
When running periodic backups on a timer, we don't want to be fsync()ing
unnecessarily.
* Skip backup if modification time has not changed
We don't want the user leaving Anki open overnight, and coming back
to lots of identical backups.
* Periodic backups
Creates an automatic backup every 30 minutes if the collection has been
modified.
If there's a legacy checkpoint active, tries again 5 minutes later.
* Switch to a user-configurable backup duration
CreateBackup() now uses a simple force argument to determine whether
the user's limits should be respected or not, and only potentially
destructive ops (full download, check DB) override the user's configured
limit.
I considered having a separate limit for collection close and automatic
backups (eg keeping the previous 5 minute limit for collection close),
but that had two downsides:
- When the user closes their collection at the end of the day, they'd
get a recent backup. When they open the collection the next day, it
would get backed up again within 5 minutes, even though not much had
changed.
- Multiple limits are harder to communicate to users in the UI
Some remaining decisions I wasn't 100% sure about:
- If force is true but the collection has not been modified, the backup
will be skipped. If the user manually deleted their backups without
closing Anki, they wouldn't get a new one if the mtime hadn't changed.
- Force takes preference over the configured backup interval - should
we be ignored the user here, or take no backups at all?
Did a sneaky edit of the existing ftl string, as it hasn't been live
long.
* Move maybe_backup() into Collection
* Use a single method for manual and periodic backups
When manually creating a backup via the File menu, we no longer make
the user wait until the backup completes. As we continue waiting for
the backup in the background, if any errors occur, the user will get
notified about it fairly quickly.
* Show message to user if backup was skipped due to no changes
+ Don't incorrectly assert a backup will be created on force
* Add "automatic" to description
* Ensure we backup prior to importing colpkg if collection open
The backup doesn't happen when invoked from 'open backup' in the profile
screen, which matches Anki's previous behaviour. The user could
potentially clobber up to 30 minutes of their work if they exited to
the profile screen and restored a backup, but the alternative is we
create backups every time a backup is restored, which may happen a number
of times if the user is trying various ones. Or we could go back to a
separate throttle amount for this case, at the cost of more complexity.
* Remove the 0 special case on backup interval; minimum of 5 minutes
https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
|
|
|
from anki import hooks
|
2021-01-31 06:55:08 +01:00
|
|
|
from anki._backend import RustBackend as _RustBackend
|
2021-04-06 06:36:13 +02:00
|
|
|
from anki.collection import Collection, Config, OpChanges, UndoStatus
|
2021-03-27 12:38:20 +01:00
|
|
|
from anki.decks import DeckDict, DeckId
|
2020-01-15 04:49:26 +01:00
|
|
|
from anki.hooks import runHook
|
2021-03-27 12:38:20 +01:00
|
|
|
from anki.notes import NoteId
|
2020-01-20 13:01:38 +01:00
|
|
|
from anki.sound import AVTag, SoundOrVideoTag
|
2022-01-21 12:43:54 +01:00
|
|
|
from anki.utils import (
|
|
|
|
dev_mode,
|
|
|
|
ids2str,
|
|
|
|
int_time,
|
|
|
|
is_lin,
|
|
|
|
is_mac,
|
|
|
|
is_win,
|
|
|
|
point_version,
|
|
|
|
split_fields,
|
|
|
|
)
|
2020-01-13 05:38:05 +01:00
|
|
|
from aqt import gui_hooks
|
2020-01-19 02:31:09 +01:00
|
|
|
from aqt.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_to_user
|
2020-06-08 12:28:11 +02:00
|
|
|
from aqt.dbcheck import check_db
|
2020-04-25 11:44:48 +02:00
|
|
|
from aqt.emptycards import show_empty_cards
|
2021-07-02 11:16:10 +02:00
|
|
|
from aqt.flags import FlagManager
|
2020-01-23 09:19:54 +01:00
|
|
|
from aqt.legacy import install_pylib_legacy
|
2020-02-11 06:51:30 +01:00
|
|
|
from aqt.mediacheck import check_media_db
|
2020-02-04 02:48:51 +01:00
|
|
|
from aqt.mediasync import MediaSyncer
|
Backup improvements (#1728)
* Collection needs to be closed prior to backup even when not downgrading
* Backups -> BackupLimits
* Some improvements to backup_task
- backup_inner now returns the error instead of logging it, so that
the frontend can discover the issue when they await a backup (or create
another one)
- start_backup() was acquiring backup_task twice, and if another thread
started a backup between the two locks, the task could have been accidentally
overwritten without awaiting it
* Backups no longer require a collection close
- Instead of closing the collection, we ensure there is no active
transaction, and flush the WAL to disk. This means the undo history
is no longer lost on backup, which will be particularly useful if we
add a periodic backup in the future.
- Because a close is no longer required, backups are now achieved with
a separate command, instead of being included in CloseCollection().
- Full sync no longer requires an extra close+reopen step, and we now
wait for the backup to complete before proceeding.
- Create a backup before 'check db'
* Add File>Create Backup
https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157
* Defer checkpoint until we know we need it
When running periodic backups on a timer, we don't want to be fsync()ing
unnecessarily.
* Skip backup if modification time has not changed
We don't want the user leaving Anki open overnight, and coming back
to lots of identical backups.
* Periodic backups
Creates an automatic backup every 30 minutes if the collection has been
modified.
If there's a legacy checkpoint active, tries again 5 minutes later.
* Switch to a user-configurable backup duration
CreateBackup() now uses a simple force argument to determine whether
the user's limits should be respected or not, and only potentially
destructive ops (full download, check DB) override the user's configured
limit.
I considered having a separate limit for collection close and automatic
backups (eg keeping the previous 5 minute limit for collection close),
but that had two downsides:
- When the user closes their collection at the end of the day, they'd
get a recent backup. When they open the collection the next day, it
would get backed up again within 5 minutes, even though not much had
changed.
- Multiple limits are harder to communicate to users in the UI
Some remaining decisions I wasn't 100% sure about:
- If force is true but the collection has not been modified, the backup
will be skipped. If the user manually deleted their backups without
closing Anki, they wouldn't get a new one if the mtime hadn't changed.
- Force takes preference over the configured backup interval - should
we be ignored the user here, or take no backups at all?
Did a sneaky edit of the existing ftl string, as it hasn't been live
long.
* Move maybe_backup() into Collection
* Use a single method for manual and periodic backups
When manually creating a backup via the File menu, we no longer make
the user wait until the backup completes. As we continue waiting for
the backup in the background, if any errors occur, the user will get
notified about it fairly quickly.
* Show message to user if backup was skipped due to no changes
+ Don't incorrectly assert a backup will be created on force
* Add "automatic" to description
* Ensure we backup prior to importing colpkg if collection open
The backup doesn't happen when invoked from 'open backup' in the profile
screen, which matches Anki's previous behaviour. The user could
potentially clobber up to 30 minutes of their work if they exited to
the profile screen and restored a backup, but the alternative is we
create backups every time a backup is restored, which may happen a number
of times if the user is trying various ones. Or we could go back to a
separate throttle amount for this case, at the cost of more complexity.
* Remove the 0 special case on backup interval; minimum of 5 minutes
https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
|
|
|
from aqt.operations import QueryOp
|
2021-05-19 07:18:39 +02:00
|
|
|
from aqt.operations.collection import redo, undo
|
2021-04-06 13:37:31 +02:00
|
|
|
from aqt.operations.deck import set_current_deck
|
2019-12-20 10:19:03 +01:00
|
|
|
from aqt.profiles import ProfileManager as ProfileManagerType
|
|
|
|
from aqt.qt import *
|
|
|
|
from aqt.qt import sip
|
2020-05-31 02:53:54 +02:00
|
|
|
from aqt.sync import sync_collection, sync_login
|
2020-01-19 01:05:37 +01:00
|
|
|
from aqt.taskman import TaskManager
|
2021-11-24 22:17:41 +01:00
|
|
|
from aqt.theme import Theme, theme_manager
|
2021-05-19 07:18:39 +02:00
|
|
|
from aqt.undo import UndoActionsInfo
|
2019-12-23 01:34:10 +01:00
|
|
|
from aqt.utils import (
|
2021-01-25 14:45:47 +01:00
|
|
|
HelpPage,
|
2021-03-17 05:51:59 +01:00
|
|
|
KeyboardModifiersPressed,
|
2019-12-23 01:34:10 +01:00
|
|
|
askUser,
|
|
|
|
checkInvalidFilename,
|
2021-06-01 07:16:53 +02:00
|
|
|
current_window,
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button,
|
2022-03-09 05:21:54 +01:00
|
|
|
disallow_full_screen,
|
2019-12-23 01:34:10 +01:00
|
|
|
getFile,
|
|
|
|
getOnlyText,
|
|
|
|
openHelp,
|
|
|
|
openLink,
|
|
|
|
restoreGeom,
|
2020-05-31 01:58:00 +02:00
|
|
|
restoreSplitter,
|
2019-12-23 01:34:10 +01:00
|
|
|
restoreState,
|
2020-05-31 01:58:00 +02:00
|
|
|
saveGeom,
|
|
|
|
saveSplitter,
|
2021-12-13 05:10:24 +01:00
|
|
|
saveState,
|
2019-12-23 01:34:10 +01:00
|
|
|
showInfo,
|
|
|
|
showWarning,
|
|
|
|
tooltip,
|
2020-02-27 11:32:57 +01:00
|
|
|
tr,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2021-12-20 11:22:21 +01:00
|
|
|
from aqt.webview import AnkiWebView
|
2013-12-06 05:27:13 +01:00
|
|
|
|
2020-01-23 09:19:54 +01:00
|
|
|
install_pylib_legacy()
|
|
|
|
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
MainWindowState = Literal[
|
|
|
|
"startup", "deckBrowser", "overview", "review", "resetRequired", "profileManager"
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2021-04-06 04:47:55 +02:00
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
|
|
|
2021-12-20 11:22:21 +01:00
|
|
|
class MainWebView(AnkiWebView):
|
|
|
|
def __init__(self, mw: AnkiQt) -> None:
|
|
|
|
AnkiWebView.__init__(self, title="main webview")
|
|
|
|
self.mw = mw
|
|
|
|
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
|
|
|
|
self.setMinimumWidth(400)
|
|
|
|
self.setAcceptDrops(True)
|
|
|
|
|
|
|
|
# Importing files via drag & drop
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
|
|
|
|
if self.mw.state != "deckBrowser":
|
|
|
|
return super().dragEnterEvent(event)
|
|
|
|
mime = event.mimeData()
|
|
|
|
if not mime.hasUrls():
|
|
|
|
return
|
|
|
|
for url in mime.urls():
|
|
|
|
path = url.toLocalFile()
|
|
|
|
if not os.path.exists(path) or os.path.isdir(path):
|
|
|
|
return
|
|
|
|
event.accept()
|
|
|
|
|
|
|
|
def dropEvent(self, event: QDropEvent) -> None:
|
|
|
|
import aqt.importing
|
|
|
|
|
|
|
|
if self.mw.state != "deckBrowser":
|
|
|
|
return super().dropEvent(event)
|
|
|
|
mime = event.mimeData()
|
|
|
|
paths = [url.toLocalFile() for url in mime.urls()]
|
|
|
|
deck_paths = filter(lambda p: not p.endswith(".colpkg"), paths)
|
|
|
|
for path in deck_paths:
|
|
|
|
aqt.importing.importFile(self.mw, path)
|
2022-01-02 00:49:51 +01:00
|
|
|
# importing continues after the above call returns, so it is not
|
|
|
|
# currently safe for us to import more than one file at once
|
|
|
|
return
|
2021-12-20 11:22:21 +01:00
|
|
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
class AnkiQt(QMainWindow):
|
2020-05-20 09:56:52 +02:00
|
|
|
col: Collection
|
2019-12-20 09:43:52 +01:00
|
|
|
pm: ProfileManagerType
|
2021-12-20 11:22:21 +01:00
|
|
|
web: MainWebView
|
|
|
|
bottomWeb: AnkiWebView
|
2019-12-20 09:43:52 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
2021-03-17 05:51:59 +01:00
|
|
|
app: aqt.AnkiApp,
|
2019-12-23 01:34:10 +01:00
|
|
|
profileManager: ProfileManagerType,
|
2021-01-31 06:55:08 +01:00
|
|
|
backend: _RustBackend,
|
2019-12-23 01:34:10 +01:00
|
|
|
opts: Namespace,
|
2021-10-03 10:59:42 +02:00
|
|
|
args: list[Any],
|
2019-12-23 01:34:10 +01:00
|
|
|
) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
QMainWindow.__init__(self)
|
2020-03-14 00:45:00 +01:00
|
|
|
self.backend = backend
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
self.state: MainWindowState = "startup"
|
2017-10-03 04:12:57 +02:00
|
|
|
self.opts = opts
|
2021-10-03 10:59:42 +02:00
|
|
|
self.col: Collection | None = None
|
2020-05-04 13:30:41 +02:00
|
|
|
self.taskman = TaskManager(self)
|
2020-02-04 03:26:10 +01:00
|
|
|
self.media_syncer = MediaSyncer(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
aqt.mw = self
|
|
|
|
self.app = app
|
2019-12-19 02:39:40 +01:00
|
|
|
self.pm = profileManager
|
2012-12-21 08:51:59 +01:00
|
|
|
# init rest of app
|
2021-05-18 01:16:25 +02:00
|
|
|
self.safeMode = (
|
2021-10-05 05:53:01 +02:00
|
|
|
bool(self.app.queryKeyboardModifiers() & Qt.KeyboardModifier.ShiftModifier)
|
2021-05-18 01:16:25 +02:00
|
|
|
or self.opts.safemode
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
try:
|
|
|
|
self.setupUI()
|
2020-01-04 04:34:16 +01:00
|
|
|
self.setupAddons(args)
|
2020-07-02 02:23:14 +02:00
|
|
|
self.finish_ui_setup()
|
2012-12-21 08:51:59 +01:00
|
|
|
except:
|
2021-03-26 05:21:04 +01:00
|
|
|
showInfo(tr.qt_misc_error_during_startup(val=traceback.format_exc()))
|
2012-12-21 08:51:59 +01:00
|
|
|
sys.exit(1)
|
2013-05-17 08:32:17 +02:00
|
|
|
# must call this after ui set up
|
|
|
|
if self.safeMode:
|
2021-03-26 04:48:26 +01:00
|
|
|
tooltip(tr.qt_misc_shift_key_was_held_down_skipping())
|
2012-12-21 08:51:59 +01:00
|
|
|
# were we given a file to import?
|
2020-01-04 04:34:16 +01:00
|
|
|
if args and args[0] and not self._isAddon(args[0]):
|
2016-05-12 06:45:35 +02:00
|
|
|
self.onAppMsg(args[0])
|
2012-12-21 08:51:59 +01:00
|
|
|
# Load profile in a timer so we can let the window finish init and not
|
|
|
|
# close on profile load error.
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_win:
|
2019-04-16 05:24:38 +02:00
|
|
|
fn = self.setupProfileAfterWebviewsLoaded
|
|
|
|
else:
|
|
|
|
fn = self.setupProfile
|
2020-05-28 13:30:22 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def on_window_init() -> None:
|
2020-05-28 13:30:22 +02:00
|
|
|
fn()
|
|
|
|
gui_hooks.main_window_did_init()
|
|
|
|
|
2022-02-24 12:15:56 +01:00
|
|
|
self.progress.single_shot(10, on_window_init, False)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupUI(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.col = None
|
2021-03-05 04:07:52 +01:00
|
|
|
self.disable_automatic_garbage_collection()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setupAppMsg()
|
|
|
|
self.setupKeys()
|
|
|
|
self.setupThreads()
|
2017-07-28 08:19:06 +02:00
|
|
|
self.setupMediaServer()
|
2019-03-06 14:18:26 +01:00
|
|
|
self.setupSpellCheck()
|
2021-11-24 22:17:41 +01:00
|
|
|
self.setupProgress()
|
2020-01-23 06:08:10 +01:00
|
|
|
self.setupStyle()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setupMainWindow()
|
|
|
|
self.setupSystemSpecific()
|
|
|
|
self.setupMenus()
|
|
|
|
self.setupErrorHandler()
|
|
|
|
self.setupSignals()
|
|
|
|
self.setupAutoUpdate()
|
2013-05-22 05:27:37 +02:00
|
|
|
self.setupHooks()
|
2020-02-05 03:38:36 +01:00
|
|
|
self.setup_timers()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.updateTitleBar()
|
2021-03-14 13:08:37 +01:00
|
|
|
self.setup_focus()
|
2012-12-21 08:51:59 +01:00
|
|
|
# screens
|
|
|
|
self.setupDeckBrowser()
|
|
|
|
self.setupOverview()
|
|
|
|
self.setupReviewer()
|
|
|
|
|
2020-07-02 02:23:14 +02:00
|
|
|
def finish_ui_setup(self) -> None:
|
|
|
|
"Actions that are deferred until after add-on loading."
|
|
|
|
self.toolbar.draw()
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def setupProfileAfterWebviewsLoaded(self) -> None:
|
2019-04-16 05:24:38 +02:00
|
|
|
for w in (self.web, self.bottomWeb):
|
|
|
|
if not w._domDone:
|
2022-02-24 12:15:56 +01:00
|
|
|
self.progress.single_shot(
|
2019-12-23 01:34:10 +01:00
|
|
|
10,
|
|
|
|
self.setupProfileAfterWebviewsLoaded,
|
|
|
|
False,
|
|
|
|
)
|
2019-04-16 05:24:38 +02:00
|
|
|
return
|
|
|
|
else:
|
|
|
|
w.requiresCol = True
|
|
|
|
|
|
|
|
self.setupProfile()
|
|
|
|
|
2020-04-25 11:44:48 +02:00
|
|
|
def weakref(self) -> AnkiQt:
|
|
|
|
"Shortcut to create a weak reference that doesn't break code completion."
|
|
|
|
return weakref.proxy(self) # type: ignore
|
|
|
|
|
2021-03-14 13:08:37 +01:00
|
|
|
def setup_focus(self) -> None:
|
|
|
|
qconnect(self.app.focusChanged, self.on_focus_changed)
|
|
|
|
|
|
|
|
def on_focus_changed(self, old: QWidget, new: QWidget) -> None:
|
|
|
|
gui_hooks.focus_did_change(new, old)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Profiles
|
|
|
|
##########################################################################
|
|
|
|
|
2017-08-16 11:45:39 +02:00
|
|
|
class ProfileManager(QMainWindow):
|
|
|
|
onClose = pyqtSignal()
|
|
|
|
closeFires = True
|
|
|
|
|
2020-08-01 04:27:54 +02:00
|
|
|
def closeEvent(self, evt: QCloseEvent) -> None:
|
2017-08-16 11:45:39 +02:00
|
|
|
if self.closeFires:
|
2020-08-01 04:27:54 +02:00
|
|
|
self.onClose.emit() # type: ignore
|
2017-08-16 11:45:39 +02:00
|
|
|
evt.accept()
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def closeWithoutQuitting(self) -> None:
|
2017-08-16 11:45:39 +02:00
|
|
|
self.closeFires = False
|
|
|
|
self.close()
|
|
|
|
self.closeFires = True
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupProfile(self) -> None:
|
2019-12-23 01:34:10 +01:00
|
|
|
if self.pm.meta["firstRun"]:
|
2019-04-09 10:48:50 +02:00
|
|
|
# load the new deck user profile
|
|
|
|
self.pm.load(self.pm.profiles()[0])
|
2019-12-23 01:34:10 +01:00
|
|
|
self.pm.meta["firstRun"] = False
|
2019-04-09 10:48:50 +02:00
|
|
|
self.pm.save()
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
self.pendingImport: str | None = None
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
self.restoring_backup = False
|
2012-12-21 08:51:59 +01:00
|
|
|
# profile not provided on command line?
|
|
|
|
if not self.pm.name:
|
|
|
|
# if there's a single profile, load it automatically
|
|
|
|
profs = self.pm.profiles()
|
|
|
|
if len(profs) == 1:
|
2017-08-16 11:45:39 +02:00
|
|
|
self.pm.load(profs[0])
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.pm.name:
|
|
|
|
self.showProfileManager()
|
|
|
|
else:
|
|
|
|
self.loadProfile()
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def showProfileManager(self) -> None:
|
2017-08-16 06:38:55 +02:00
|
|
|
self.pm.profile = None
|
2022-03-21 05:45:22 +01:00
|
|
|
self.moveToState("profileManager")
|
2017-08-16 11:45:39 +02:00
|
|
|
d = self.profileDiag = self.ProfileManager()
|
|
|
|
f = self.profileForm = aqt.forms.profiles.Ui_MainWindow()
|
2012-12-21 08:51:59 +01:00
|
|
|
f.setupUi(d)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(f.login.clicked, self.onOpenProfile)
|
|
|
|
qconnect(f.profiles.itemDoubleClicked, self.onOpenProfile)
|
|
|
|
qconnect(f.openBackup.clicked, self.onOpenBackup)
|
|
|
|
qconnect(f.quit.clicked, d.close)
|
|
|
|
qconnect(d.onClose, self.cleanupAndExit)
|
|
|
|
qconnect(f.add.clicked, self.onAddProfile)
|
|
|
|
qconnect(f.rename.clicked, self.onRenameProfile)
|
|
|
|
qconnect(f.delete_2.clicked, self.onRemProfile)
|
|
|
|
qconnect(f.profiles.currentRowChanged, self.onProfileRowChange)
|
2017-08-16 11:45:39 +02:00
|
|
|
f.statusbar.setVisible(False)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(f.downgrade_button.clicked, self._on_downgrade)
|
2021-03-26 04:48:26 +01:00
|
|
|
f.downgrade_button.setText(tr.profiles_downgrade_and_quit())
|
2017-08-16 11:45:39 +02:00
|
|
|
# enter key opens profile
|
2019-12-23 01:34:10 +01:00
|
|
|
QShortcut(QKeySequence("Return"), d, activated=self.onOpenProfile) # type: ignore
|
2012-12-21 08:51:59 +01:00
|
|
|
self.refreshProfilesList()
|
|
|
|
# raise first, for osx testing
|
|
|
|
d.show()
|
2018-02-01 03:14:04 +01:00
|
|
|
d.activateWindow()
|
|
|
|
d.raise_()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def refreshProfilesList(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
f = self.profileForm
|
|
|
|
f.profiles.clear()
|
|
|
|
profs = self.pm.profiles()
|
|
|
|
f.profiles.addItems(profs)
|
|
|
|
try:
|
|
|
|
idx = profs.index(self.pm.name)
|
|
|
|
except:
|
|
|
|
idx = 0
|
|
|
|
f.profiles.setCurrentRow(idx)
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def onProfileRowChange(self, n: int) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if n < 0:
|
|
|
|
# called on .clear()
|
|
|
|
return
|
|
|
|
name = self.pm.profiles()[n]
|
2017-08-16 11:45:39 +02:00
|
|
|
self.pm.load(name)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def openProfile(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
name = self.pm.profiles()[self.profileForm.profiles.currentRow()]
|
2021-02-01 14:28:21 +01:00
|
|
|
self.pm.load(name)
|
|
|
|
return
|
2012-12-21 08:51:59 +01:00
|
|
|
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
def onOpenProfile(self, *, callback: Callable[[], None] | None = None) -> None:
|
|
|
|
def on_done() -> None:
|
|
|
|
self.profileDiag.closeWithoutQuitting()
|
|
|
|
if callback:
|
|
|
|
callback()
|
|
|
|
|
2020-02-10 00:32:56 +01:00
|
|
|
self.profileDiag.hide()
|
|
|
|
# code flow is confusing here - if load fails, profile dialog
|
|
|
|
# will be shown again
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
self.loadProfile(on_done)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-06-05 15:44:59 +02:00
|
|
|
def profileNameOk(self, name: str) -> bool:
|
|
|
|
return not checkInvalidFilename(name) and name != "addons21"
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onAddProfile(self) -> None:
|
2021-03-26 04:48:26 +01:00
|
|
|
name = getOnlyText(tr.actions_name()).strip()
|
2012-12-21 08:51:59 +01:00
|
|
|
if name:
|
|
|
|
if name in self.pm.profiles():
|
2021-03-26 04:48:26 +01:00
|
|
|
showWarning(tr.qt_misc_name_exists())
|
2021-02-01 14:28:21 +01:00
|
|
|
return
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.profileNameOk(name):
|
|
|
|
return
|
|
|
|
self.pm.create(name)
|
|
|
|
self.pm.name = name
|
|
|
|
self.refreshProfilesList()
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onRenameProfile(self) -> None:
|
2021-03-26 04:48:26 +01:00
|
|
|
name = getOnlyText(tr.actions_new_name(), default=self.pm.name).strip()
|
2012-12-21 08:51:59 +01:00
|
|
|
if not name:
|
|
|
|
return
|
|
|
|
if name == self.pm.name:
|
|
|
|
return
|
|
|
|
if name in self.pm.profiles():
|
2021-03-26 04:48:26 +01:00
|
|
|
showWarning(tr.qt_misc_name_exists())
|
2021-02-01 14:28:21 +01:00
|
|
|
return
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.profileNameOk(name):
|
|
|
|
return
|
|
|
|
self.pm.rename(name)
|
|
|
|
self.refreshProfilesList()
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onRemProfile(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
profs = self.pm.profiles()
|
|
|
|
if len(profs) < 2:
|
2021-03-26 04:48:26 +01:00
|
|
|
showWarning(tr.qt_misc_there_must_be_at_least_one())
|
2021-02-01 14:28:21 +01:00
|
|
|
return
|
2012-12-21 08:51:59 +01:00
|
|
|
# sure?
|
2019-12-23 01:34:10 +01:00
|
|
|
if not askUser(
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.qt_misc_all_cards_notes_and_media_for(),
|
2019-12-23 01:34:10 +01:00
|
|
|
msgfunc=QMessageBox.warning,
|
|
|
|
defaultno=True,
|
|
|
|
):
|
2012-12-21 08:51:59 +01:00
|
|
|
return
|
|
|
|
self.pm.remove(self.pm.name)
|
|
|
|
self.refreshProfilesList()
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onOpenBackup(self) -> None:
|
2019-12-23 01:34:10 +01:00
|
|
|
if not askUser(
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.qt_misc_replace_your_collection_with_an_earlier(),
|
2019-12-23 01:34:10 +01:00
|
|
|
msgfunc=QMessageBox.warning,
|
|
|
|
defaultno=True,
|
|
|
|
):
|
2017-08-16 11:45:39 +02:00
|
|
|
return
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def doOpen(path: str) -> None:
|
2017-08-16 11:45:39 +02:00
|
|
|
self._openBackup(path)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
|
|
|
getFile(
|
|
|
|
self.profileDiag,
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.qt_misc_revert_to_backup(),
|
2021-02-02 14:30:53 +01:00
|
|
|
cb=doOpen, # type: ignore
|
2019-12-23 01:34:10 +01:00
|
|
|
filter="*.colpkg",
|
|
|
|
dir=self.pm.backupFolder(),
|
|
|
|
)
|
2017-08-16 11:45:39 +02:00
|
|
|
|
2021-02-02 15:00:29 +01:00
|
|
|
def _openBackup(self, path: str) -> None:
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
def on_done(success: bool) -> None:
|
|
|
|
if success:
|
|
|
|
self.onOpenProfile(callback=lambda: self.col.mod_schema(check=False))
|
2017-08-16 11:45:39 +02:00
|
|
|
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
import aqt.importing
|
2017-08-16 11:45:39 +02:00
|
|
|
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
self.restoring_backup = True
|
2021-03-26 04:48:26 +01:00
|
|
|
showInfo(tr.qt_misc_automatic_syncing_and_backups_have_been())
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
aqt.importing.replace_with_apkg(self, path, on_done)
|
2017-08-16 11:45:39 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def _on_downgrade(self) -> None:
|
2020-04-16 01:00:49 +02:00
|
|
|
self.progress.start()
|
|
|
|
profiles = self.pm.profiles()
|
2020-04-16 01:53:29 +02:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def downgrade() -> list[str]:
|
2020-04-16 01:53:29 +02:00
|
|
|
return self.pm.downgrade(profiles)
|
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def on_done(future: Future) -> None:
|
2020-04-16 01:00:49 +02:00
|
|
|
self.progress.finish()
|
2020-04-16 01:53:29 +02:00
|
|
|
problems = future.result()
|
|
|
|
if not problems:
|
|
|
|
showInfo("Profiles can now be opened with an older version of Anki.")
|
|
|
|
else:
|
|
|
|
showWarning(
|
|
|
|
"The following profiles could not be downgraded: {}".format(
|
|
|
|
", ".join(problems)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return
|
2020-04-16 01:00:49 +02:00
|
|
|
self.profileDiag.close()
|
2020-04-16 01:53:29 +02:00
|
|
|
|
2020-04-16 01:00:49 +02:00
|
|
|
self.taskman.run_in_background(downgrade, on_done)
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def loadProfile(self, onsuccess: Callable | None = None) -> None:
|
2017-08-16 06:38:55 +02:00
|
|
|
if not self.loadCollection():
|
|
|
|
return
|
|
|
|
|
2022-02-11 01:35:48 +01:00
|
|
|
self.setup_sound()
|
2021-08-30 11:07:40 +02:00
|
|
|
self.flags = FlagManager(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
# show main window
|
2019-12-23 01:34:10 +01:00
|
|
|
if self.pm.profile["mainWindowState"]:
|
2012-12-21 08:51:59 +01:00
|
|
|
restoreGeom(self, "mainWindow")
|
|
|
|
restoreState(self, "mainWindow")
|
2013-05-22 06:04:45 +02:00
|
|
|
# titlebar
|
2021-02-11 01:09:06 +01:00
|
|
|
self.setWindowTitle(f"{self.pm.name} - Anki")
|
2012-12-21 08:51:59 +01:00
|
|
|
# show and raise window for osx
|
|
|
|
self.show()
|
|
|
|
self.activateWindow()
|
|
|
|
self.raise_()
|
2017-08-16 06:38:55 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# import pending?
|
|
|
|
if self.pendingImport:
|
2020-01-04 04:30:33 +01:00
|
|
|
if self._isAddon(self.pendingImport):
|
|
|
|
self.installAddon(self.pendingImport)
|
|
|
|
else:
|
|
|
|
self.handleImport(self.pendingImport)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.pendingImport = None
|
2020-01-15 07:53:24 +01:00
|
|
|
gui_hooks.profile_did_open()
|
2020-05-31 02:53:54 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def _onsuccess() -> None:
|
2020-12-01 01:20:55 +01:00
|
|
|
self._refresh_after_sync()
|
|
|
|
if onsuccess:
|
|
|
|
onsuccess()
|
|
|
|
|
|
|
|
self.maybe_auto_sync_on_open_close(_onsuccess)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def unloadProfile(self, onsuccess: Callable) -> None:
|
2021-02-01 14:28:21 +01:00
|
|
|
def callback() -> None:
|
2017-08-16 06:38:55 +02:00
|
|
|
self._unloadProfile()
|
|
|
|
onsuccess()
|
|
|
|
|
2020-01-15 07:53:24 +01:00
|
|
|
gui_hooks.profile_will_close()
|
2017-08-16 06:38:55 +02:00
|
|
|
self.unloadCollection(callback)
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def _unloadProfile(self) -> None:
|
2022-02-11 01:35:48 +01:00
|
|
|
self.cleanup_sound()
|
2021-12-13 05:10:24 +01:00
|
|
|
saveGeom(self, "mainWindow")
|
|
|
|
saveState(self, "mainWindow")
|
2012-12-21 08:51:59 +01:00
|
|
|
self.pm.save()
|
|
|
|
self.hide()
|
2017-08-16 06:38:55 +02:00
|
|
|
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
self.restoring_backup = False
|
2017-08-16 11:45:39 +02:00
|
|
|
|
2017-08-16 06:38:55 +02:00
|
|
|
# at this point there should be no windows left
|
|
|
|
self._checkForUnclosedWidgets()
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def _checkForUnclosedWidgets(self) -> None:
|
2017-08-16 06:38:55 +02:00
|
|
|
for w in self.app.topLevelWidgets():
|
|
|
|
if w.isVisible():
|
2017-08-25 04:14:59 +02:00
|
|
|
# windows with this property are safe to close immediately
|
2017-09-08 10:42:26 +02:00
|
|
|
if getattr(w, "silentlyClose", None):
|
2017-08-25 04:14:59 +02:00
|
|
|
w.close()
|
|
|
|
else:
|
2021-02-11 00:37:38 +01:00
|
|
|
print(f"Window should have been closed: {w}")
|
2017-08-16 06:38:55 +02:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def unloadProfileAndExit(self) -> None:
|
2017-08-16 06:38:55 +02:00
|
|
|
self.unloadProfile(self.cleanupAndExit)
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def unloadProfileAndShowProfileManager(self) -> None:
|
2017-08-16 06:38:55 +02:00
|
|
|
self.unloadProfile(self.showProfileManager)
|
2017-08-08 04:55:30 +02:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def cleanupAndExit(self) -> None:
|
2017-08-08 04:55:30 +02:00
|
|
|
self.errorHandler.unload()
|
|
|
|
self.mediaServer.shutdown()
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
# Rust background jobs are not awaited implicitly
|
|
|
|
self.backend.await_backup_completion()
|
2017-08-08 04:55:30 +02:00
|
|
|
self.app.exit(0)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-10-05 05:48:24 +02:00
|
|
|
# Sound/video
|
|
|
|
##########################################################################
|
|
|
|
|
2022-02-11 01:35:48 +01:00
|
|
|
def setup_sound(self) -> None:
|
|
|
|
aqt.sound.setup_audio(self.taskman, self.pm.base, self.col.media.dir())
|
|
|
|
|
|
|
|
def cleanup_sound(self) -> None:
|
|
|
|
aqt.sound.cleanup_audio()
|
2017-10-05 05:48:24 +02:00
|
|
|
|
2020-01-24 02:06:11 +01:00
|
|
|
def _add_play_buttons(self, text: str) -> str:
|
|
|
|
"Return card text with play buttons added, or stripped."
|
2021-03-10 09:20:37 +01:00
|
|
|
if self.col.get_config_bool(Config.Bool.HIDE_AUDIO_PLAY_BUTTONS):
|
2020-01-24 06:48:40 +01:00
|
|
|
return anki.sound.strip_av_refs(text)
|
2021-03-10 09:20:37 +01:00
|
|
|
else:
|
|
|
|
return aqt.sound.av_refs_to_play_icons(text)
|
2020-01-24 02:06:11 +01:00
|
|
|
|
|
|
|
def prepare_card_text_for_display(self, text: str) -> str:
|
2021-07-16 02:37:59 +02:00
|
|
|
text = self.col.media.escape_media_filenames(text)
|
2020-01-24 02:06:11 +01:00
|
|
|
text = self._add_play_buttons(text)
|
|
|
|
return text
|
2020-01-21 12:00:17 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Collection load/unload
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def loadCollection(self) -> bool:
|
2012-12-21 08:51:59 +01:00
|
|
|
try:
|
2020-03-26 00:53:15 +01:00
|
|
|
self._loadCollection()
|
2017-08-16 06:38:55 +02:00
|
|
|
except Exception as e:
|
2020-04-16 01:47:34 +02:00
|
|
|
if "FileTooNew" in str(e):
|
2020-04-16 01:53:29 +02:00
|
|
|
showWarning(
|
2021-03-26 02:27:22 +01:00
|
|
|
"This profile requires a newer version of Anki to open. Did you forget to use the Downgrade button prior to switching Anki versions?"
|
2020-04-16 01:53:29 +02:00
|
|
|
)
|
2020-04-16 01:47:34 +02:00
|
|
|
else:
|
|
|
|
showWarning(
|
2021-03-26 04:48:26 +01:00
|
|
|
f"{tr.errors_unable_open_collection()}\n{traceback.format_exc()}"
|
2020-04-16 01:47:34 +02:00
|
|
|
)
|
2018-02-05 03:15:57 +01:00
|
|
|
# clean up open collection if possible
|
2020-05-02 04:42:52 +02:00
|
|
|
try:
|
Backup improvements (#1728)
* Collection needs to be closed prior to backup even when not downgrading
* Backups -> BackupLimits
* Some improvements to backup_task
- backup_inner now returns the error instead of logging it, so that
the frontend can discover the issue when they await a backup (or create
another one)
- start_backup() was acquiring backup_task twice, and if another thread
started a backup between the two locks, the task could have been accidentally
overwritten without awaiting it
* Backups no longer require a collection close
- Instead of closing the collection, we ensure there is no active
transaction, and flush the WAL to disk. This means the undo history
is no longer lost on backup, which will be particularly useful if we
add a periodic backup in the future.
- Because a close is no longer required, backups are now achieved with
a separate command, instead of being included in CloseCollection().
- Full sync no longer requires an extra close+reopen step, and we now
wait for the backup to complete before proceeding.
- Create a backup before 'check db'
* Add File>Create Backup
https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157
* Defer checkpoint until we know we need it
When running periodic backups on a timer, we don't want to be fsync()ing
unnecessarily.
* Skip backup if modification time has not changed
We don't want the user leaving Anki open overnight, and coming back
to lots of identical backups.
* Periodic backups
Creates an automatic backup every 30 minutes if the collection has been
modified.
If there's a legacy checkpoint active, tries again 5 minutes later.
* Switch to a user-configurable backup duration
CreateBackup() now uses a simple force argument to determine whether
the user's limits should be respected or not, and only potentially
destructive ops (full download, check DB) override the user's configured
limit.
I considered having a separate limit for collection close and automatic
backups (eg keeping the previous 5 minute limit for collection close),
but that had two downsides:
- When the user closes their collection at the end of the day, they'd
get a recent backup. When they open the collection the next day, it
would get backed up again within 5 minutes, even though not much had
changed.
- Multiple limits are harder to communicate to users in the UI
Some remaining decisions I wasn't 100% sure about:
- If force is true but the collection has not been modified, the backup
will be skipped. If the user manually deleted their backups without
closing Anki, they wouldn't get a new one if the mtime hadn't changed.
- Force takes preference over the configured backup interval - should
we be ignored the user here, or take no backups at all?
Did a sneaky edit of the existing ftl string, as it hasn't been live
long.
* Move maybe_backup() into Collection
* Use a single method for manual and periodic backups
When manually creating a backup via the File menu, we no longer make
the user wait until the backup completes. As we continue waiting for
the backup in the background, if any errors occur, the user will get
notified about it fairly quickly.
* Show message to user if backup was skipped due to no changes
+ Don't incorrectly assert a backup will be created on force
* Add "automatic" to description
* Ensure we backup prior to importing colpkg if collection open
The backup doesn't happen when invoked from 'open backup' in the profile
screen, which matches Anki's previous behaviour. The user could
potentially clobber up to 30 minutes of their work if they exited to
the profile screen and restored a backup, but the alternative is we
create backups every time a backup is restored, which may happen a number
of times if the user is trying various ones. Or we could go back to a
separate throttle amount for this case, at the cost of more complexity.
* Remove the 0 special case on backup interval; minimum of 5 minutes
https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
|
|
|
self.backend.close_collection(downgrade_to_schema11=False)
|
2020-05-02 04:42:52 +02:00
|
|
|
except Exception as e:
|
|
|
|
print("unable to close collection:", e)
|
|
|
|
self.col = None
|
2018-02-05 03:15:57 +01:00
|
|
|
# return to profile manager
|
possible fix for " super-class ... Preferences was never called"
Can't reproduce the issue, but it seems the user was able to open the
preferences screen when no collection was loaded. If an error was
caught in loadCollection() the main window was not being hidden, so
perhaps a timing issue was preventing the profiles screen from taking
modal focus.
Removed the check in the prefs init - it is hopefully no longer
necessary, and returning before QDialog.__init__() was called was
causing the problem.
Caught exception:
File "aqt/webview.py", line 27, in cmd
File "aqt/webview.py", line 85, in _onCmd
File "aqt/webview.py", line 360, in _onBridgeCmd
File "aqt/toolbar.py", line 56, in _linkHandler
File "aqt/toolbar.py", line 80, in _syncLinkHandler
File "aqt/main.py", line 669, in onSync
File "aqt/main.py", line 365, in unloadCollection
File "aqt/main.py", line 611, in closeAllWindows
File "aqt/__init__.py", line 110, in closeAll
<class 'RuntimeError'>: super-class __init__() of type Preferences was never called
2019-04-21 11:02:03 +02:00
|
|
|
self.hide()
|
2017-08-16 06:38:55 +02:00
|
|
|
self.showProfileManager()
|
|
|
|
return False
|
|
|
|
|
2020-03-26 00:53:15 +01:00
|
|
|
# make sure we don't get into an inconsistent state if an add-on
|
|
|
|
# has broken the deck browser or the did_load hook
|
|
|
|
try:
|
2021-03-05 13:45:55 +01:00
|
|
|
self.update_undo_actions()
|
2020-03-26 00:53:15 +01:00
|
|
|
gui_hooks.collection_did_load(self.col)
|
2021-03-10 09:20:37 +01:00
|
|
|
self.apply_collection_options()
|
2020-03-26 00:53:15 +01:00
|
|
|
self.moveToState("deckBrowser")
|
|
|
|
except Exception as e:
|
|
|
|
# dump error to stderr so it gets picked up by errors.py
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def _loadCollection(self) -> None:
|
2020-05-20 09:43:34 +02:00
|
|
|
cpath = self.pm.collectionPath()
|
2021-06-27 07:12:22 +02:00
|
|
|
self.col = Collection(cpath, backend=self.backend)
|
2017-11-01 03:38:43 +01:00
|
|
|
self.setEnabled(True)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def reopen(self) -> None:
|
2020-05-20 09:43:34 +02:00
|
|
|
self.col.reopen()
|
2020-03-06 05:03:23 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def unloadCollection(self, onsuccess: Callable) -> None:
|
2021-02-01 14:28:21 +01:00
|
|
|
def after_media_sync() -> None:
|
2017-08-16 06:38:55 +02:00
|
|
|
self._unloadCollection()
|
|
|
|
onsuccess()
|
2013-04-23 15:37:21 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def after_sync() -> None:
|
2020-05-31 10:51:05 +02:00
|
|
|
self.media_syncer.show_diag_until_finished(after_media_sync)
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def before_sync() -> None:
|
2020-05-31 02:53:54 +02:00
|
|
|
self.setEnabled(False)
|
|
|
|
self.maybe_auto_sync_on_open_close(after_sync)
|
|
|
|
|
|
|
|
self.closeAllWindows(before_sync)
|
2017-08-16 06:38:55 +02:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def _unloadCollection(self) -> None:
|
2017-08-16 11:45:39 +02:00
|
|
|
if not self.col:
|
|
|
|
return
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
|
|
|
|
label = (
|
|
|
|
tr.qt_misc_closing() if self.restoring_backup else tr.qt_misc_backing_up()
|
|
|
|
)
|
2020-05-31 03:24:33 +02:00
|
|
|
self.progress.start(label=label)
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
|
2017-08-16 06:38:55 +02:00
|
|
|
corrupt = False
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
|
2017-08-16 06:38:55 +02:00
|
|
|
try:
|
|
|
|
self.maybeOptimize()
|
2021-11-25 00:06:16 +01:00
|
|
|
if not dev_mode:
|
2020-04-18 02:21:31 +02:00
|
|
|
corrupt = self.col.db.scalar("pragma quick_check") != "ok"
|
2017-08-16 06:38:55 +02:00
|
|
|
except:
|
|
|
|
corrupt = True
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
|
2017-08-16 06:38:55 +02:00
|
|
|
try:
|
Backup improvements (#1728)
* Collection needs to be closed prior to backup even when not downgrading
* Backups -> BackupLimits
* Some improvements to backup_task
- backup_inner now returns the error instead of logging it, so that
the frontend can discover the issue when they await a backup (or create
another one)
- start_backup() was acquiring backup_task twice, and if another thread
started a backup between the two locks, the task could have been accidentally
overwritten without awaiting it
* Backups no longer require a collection close
- Instead of closing the collection, we ensure there is no active
transaction, and flush the WAL to disk. This means the undo history
is no longer lost on backup, which will be particularly useful if we
add a periodic backup in the future.
- Because a close is no longer required, backups are now achieved with
a separate command, instead of being included in CloseCollection().
- Full sync no longer requires an extra close+reopen step, and we now
wait for the backup to complete before proceeding.
- Create a backup before 'check db'
* Add File>Create Backup
https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157
* Defer checkpoint until we know we need it
When running periodic backups on a timer, we don't want to be fsync()ing
unnecessarily.
* Skip backup if modification time has not changed
We don't want the user leaving Anki open overnight, and coming back
to lots of identical backups.
* Periodic backups
Creates an automatic backup every 30 minutes if the collection has been
modified.
If there's a legacy checkpoint active, tries again 5 minutes later.
* Switch to a user-configurable backup duration
CreateBackup() now uses a simple force argument to determine whether
the user's limits should be respected or not, and only potentially
destructive ops (full download, check DB) override the user's configured
limit.
I considered having a separate limit for collection close and automatic
backups (eg keeping the previous 5 minute limit for collection close),
but that had two downsides:
- When the user closes their collection at the end of the day, they'd
get a recent backup. When they open the collection the next day, it
would get backed up again within 5 minutes, even though not much had
changed.
- Multiple limits are harder to communicate to users in the UI
Some remaining decisions I wasn't 100% sure about:
- If force is true but the collection has not been modified, the backup
will be skipped. If the user manually deleted their backups without
closing Anki, they wouldn't get a new one if the mtime hadn't changed.
- Force takes preference over the configured backup interval - should
we be ignored the user here, or take no backups at all?
Did a sneaky edit of the existing ftl string, as it hasn't been live
long.
* Move maybe_backup() into Collection
* Use a single method for manual and periodic backups
When manually creating a backup via the File menu, we no longer make
the user wait until the backup completes. As we continue waiting for
the backup in the background, if any errors occur, the user will get
notified about it fairly quickly.
* Show message to user if backup was skipped due to no changes
+ Don't incorrectly assert a backup will be created on force
* Add "automatic" to description
* Ensure we backup prior to importing colpkg if collection open
The backup doesn't happen when invoked from 'open backup' in the profile
screen, which matches Anki's previous behaviour. The user could
potentially clobber up to 30 minutes of their work if they exited to
the profile screen and restored a backup, but the alternative is we
create backups every time a backup is restored, which may happen a number
of times if the user is trying various ones. Or we could go back to a
separate throttle amount for this case, at the cost of more complexity.
* Remove the 0 special case on backup interval; minimum of 5 minutes
https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
|
|
|
if not corrupt and not dev_mode and not self.restoring_backup:
|
|
|
|
# default 5 minute throttle
|
|
|
|
self.col.create_backup(
|
|
|
|
backup_folder=self.pm.backupFolder(),
|
|
|
|
force=False,
|
|
|
|
wait_for_completion=False,
|
|
|
|
)
|
|
|
|
self.col.close(downgrade=False)
|
2020-04-08 02:05:33 +02:00
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
2017-08-16 13:06:50 +02:00
|
|
|
corrupt = True
|
2017-08-16 11:45:39 +02:00
|
|
|
finally:
|
|
|
|
self.col = None
|
2020-04-09 06:19:20 +02:00
|
|
|
self.progress.finish()
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
|
2017-08-16 13:06:50 +02:00
|
|
|
if corrupt:
|
2021-03-26 04:48:26 +01:00
|
|
|
showWarning(tr.qt_misc_your_collection_file_appears_to_be())
|
2017-08-16 11:45:39 +02:00
|
|
|
|
2021-02-06 10:01:48 +01:00
|
|
|
def _close_for_full_download(self) -> None:
|
|
|
|
"Backup and prepare collection to be overwritten."
|
Backup improvements (#1728)
* Collection needs to be closed prior to backup even when not downgrading
* Backups -> BackupLimits
* Some improvements to backup_task
- backup_inner now returns the error instead of logging it, so that
the frontend can discover the issue when they await a backup (or create
another one)
- start_backup() was acquiring backup_task twice, and if another thread
started a backup between the two locks, the task could have been accidentally
overwritten without awaiting it
* Backups no longer require a collection close
- Instead of closing the collection, we ensure there is no active
transaction, and flush the WAL to disk. This means the undo history
is no longer lost on backup, which will be particularly useful if we
add a periodic backup in the future.
- Because a close is no longer required, backups are now achieved with
a separate command, instead of being included in CloseCollection().
- Full sync no longer requires an extra close+reopen step, and we now
wait for the backup to complete before proceeding.
- Create a backup before 'check db'
* Add File>Create Backup
https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157
* Defer checkpoint until we know we need it
When running periodic backups on a timer, we don't want to be fsync()ing
unnecessarily.
* Skip backup if modification time has not changed
We don't want the user leaving Anki open overnight, and coming back
to lots of identical backups.
* Periodic backups
Creates an automatic backup every 30 minutes if the collection has been
modified.
If there's a legacy checkpoint active, tries again 5 minutes later.
* Switch to a user-configurable backup duration
CreateBackup() now uses a simple force argument to determine whether
the user's limits should be respected or not, and only potentially
destructive ops (full download, check DB) override the user's configured
limit.
I considered having a separate limit for collection close and automatic
backups (eg keeping the previous 5 minute limit for collection close),
but that had two downsides:
- When the user closes their collection at the end of the day, they'd
get a recent backup. When they open the collection the next day, it
would get backed up again within 5 minutes, even though not much had
changed.
- Multiple limits are harder to communicate to users in the UI
Some remaining decisions I wasn't 100% sure about:
- If force is true but the collection has not been modified, the backup
will be skipped. If the user manually deleted their backups without
closing Anki, they wouldn't get a new one if the mtime hadn't changed.
- Force takes preference over the configured backup interval - should
we be ignored the user here, or take no backups at all?
Did a sneaky edit of the existing ftl string, as it hasn't been live
long.
* Move maybe_backup() into Collection
* Use a single method for manual and periodic backups
When manually creating a backup via the File menu, we no longer make
the user wait until the backup completes. As we continue waiting for
the backup in the background, if any errors occur, the user will get
notified about it fairly quickly.
* Show message to user if backup was skipped due to no changes
+ Don't incorrectly assert a backup will be created on force
* Add "automatic" to description
* Ensure we backup prior to importing colpkg if collection open
The backup doesn't happen when invoked from 'open backup' in the profile
screen, which matches Anki's previous behaviour. The user could
potentially clobber up to 30 minutes of their work if they exited to
the profile screen and restored a backup, but the alternative is we
create backups every time a backup is restored, which may happen a number
of times if the user is trying various ones. Or we could go back to a
separate throttle amount for this case, at the cost of more complexity.
* Remove the 0 special case on backup interval; minimum of 5 minutes
https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
|
|
|
self.create_backup_now()
|
2021-02-06 10:01:48 +01:00
|
|
|
self.col.close_for_full_sync()
|
|
|
|
|
2021-03-10 09:20:37 +01:00
|
|
|
def apply_collection_options(self) -> None:
|
|
|
|
"Setup audio after collection loaded."
|
|
|
|
aqt.sound.av_player.interrupt_current_audio = self.col.get_config_bool(
|
|
|
|
Config.Bool.INTERRUPT_AUDIO_WHEN_ANSWERING
|
|
|
|
)
|
|
|
|
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
# Auto-optimize
|
2012-12-21 08:51:59 +01:00
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def maybeOptimize(self) -> None:
|
2013-05-07 08:17:46 +02:00
|
|
|
# have two weeks passed?
|
2021-10-25 06:50:13 +02:00
|
|
|
if (int_time() - self.pm.profile["lastOptimize"]) < 86400 * 14:
|
2012-12-21 08:51:59 +01:00
|
|
|
return
|
2021-03-26 04:48:26 +01:00
|
|
|
self.progress.start(label=tr.qt_misc_optimizing())
|
2012-12-21 08:51:59 +01:00
|
|
|
self.col.optimize()
|
2021-10-25 06:50:13 +02:00
|
|
|
self.pm.profile["lastOptimize"] = int_time()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.pm.save()
|
|
|
|
self.progress.finish()
|
|
|
|
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
# Tracking main window state (deck browser, reviewer, etc)
|
2012-12-21 08:51:59 +01:00
|
|
|
##########################################################################
|
|
|
|
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
def moveToState(self, state: MainWindowState, *args: Any) -> None:
|
2019-12-23 01:34:10 +01:00
|
|
|
# print("-> move from", self.state, "to", state)
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
oldState = self.state
|
2021-02-11 01:09:06 +01:00
|
|
|
cleanup = getattr(self, f"_{oldState}Cleanup", None)
|
2012-12-21 08:51:59 +01:00
|
|
|
if cleanup:
|
2019-03-04 02:22:40 +01:00
|
|
|
# pylint: disable=not-callable
|
2012-12-21 08:51:59 +01:00
|
|
|
cleanup(state)
|
2017-06-22 08:36:54 +02:00
|
|
|
self.clearStateShortcuts()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.state = state
|
2020-01-15 07:53:24 +01:00
|
|
|
gui_hooks.state_will_change(state, oldState)
|
2022-03-21 05:45:22 +01:00
|
|
|
getattr(self, f"_{state}State", lambda *_: None)(oldState, *args)
|
2017-01-17 05:05:05 +01:00
|
|
|
if state != "resetRequired":
|
|
|
|
self.bottomWeb.show()
|
2020-01-15 07:53:24 +01:00
|
|
|
gui_hooks.state_did_change(state, oldState)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def _deckBrowserState(self, oldState: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.deckBrowser.show()
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def _selectedDeck(self) -> DeckDict | None:
|
2012-12-21 08:51:59 +01:00
|
|
|
did = self.col.decks.selected()
|
Simplify note adding and the deck/notetype choosers
The existing code was really difficult to reason about:
- The default notetype depended on the selected deck, and vice versa,
and this logic was buried in the deck and notetype choosing screens,
and models.py.
- Changes to the notetype were not passed back directly, but were fired
via a hook, which changed any screen in the app that had a notetype
selector.
It also wasn't great for performance, as the most recent deck and tags
were embedded in the notetype, which can be expensive to save and sync
for large notetypes.
To address these points:
- The current deck for a notetype, and notetype for a deck, are now
stored in separate config variables, instead of directly in the deck
or notetype. These are cheap to read and write, and we'll be able to
sync them individually in the future once config syncing is updated in
the future. I seem to recall some users not wanting the tag saving
behaviour, so I've dropped that for now, but if people end up missing
it, it would be simple to add as an extra auxiliary config variable.
- The logic for getting the starting deck and notetype has been moved
into the backend. It should be the same as the older Python code, with
one exception: when "change deck depending on notetype" is enabled in
the preferences, it will start with the current notetype ("curModel"),
instead of first trying to get a deck-specific notetype.
- ModelChooser has been duplicated into notetypechooser.py, and it
has been updated to solely be concerned with keeping track of a selected
notetype - it no longer alters global state.
2021-03-08 14:23:24 +01:00
|
|
|
if not self.col.decks.name_if_exists(did):
|
2021-03-26 04:48:26 +01:00
|
|
|
showInfo(tr.qt_misc_please_select_a_deck())
|
2019-12-20 09:43:52 +01:00
|
|
|
return None
|
2012-12-21 08:51:59 +01:00
|
|
|
return self.col.decks.get(did)
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def _overviewState(self, oldState: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self._selectedDeck():
|
|
|
|
return self.moveToState("deckBrowser")
|
|
|
|
self.overview.show()
|
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def _reviewState(self, oldState: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.reviewer.show()
|
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def _reviewCleanup(self, newState: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if newState != "resetRequired" and newState != "review":
|
|
|
|
self.reviewer.cleanup()
|
|
|
|
|
|
|
|
# Resetting state
|
|
|
|
##########################################################################
|
|
|
|
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
def _increase_background_ops(self) -> None:
|
|
|
|
if not self._background_op_count:
|
2021-03-18 01:54:02 +01:00
|
|
|
gui_hooks.backend_will_block()
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
self._background_op_count += 1
|
|
|
|
|
|
|
|
def _decrease_background_ops(self) -> None:
|
|
|
|
self._background_op_count -= 1
|
|
|
|
if not self._background_op_count:
|
2021-03-18 01:54:02 +01:00
|
|
|
gui_hooks.backend_did_block()
|
2021-11-25 08:47:50 +01:00
|
|
|
if not self._background_op_count >= 0:
|
|
|
|
raise Exception("no background ops active")
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
def _synthesize_op_did_execute_from_reset(self) -> None:
|
|
|
|
"""Fire the `operation_did_execute` hook with everything marked as changed,
|
|
|
|
after legacy code has called .reset()"""
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
op = OpChanges()
|
|
|
|
for field in op.DESCRIPTOR.fields:
|
|
|
|
if field.name != "kind":
|
|
|
|
setattr(op, field.name, True)
|
2021-04-05 05:43:09 +02:00
|
|
|
gui_hooks.operation_did_execute(op, None)
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
|
2021-04-06 02:14:11 +02:00
|
|
|
def on_operation_did_execute(
|
2021-10-03 10:59:42 +02:00
|
|
|
self, changes: OpChanges, handler: object | None
|
2021-04-06 02:14:11 +02:00
|
|
|
) -> None:
|
2021-03-14 13:08:37 +01:00
|
|
|
"Notify current screen of changes."
|
2021-06-01 07:16:53 +02:00
|
|
|
focused = current_window() == self
|
2021-03-14 13:08:37 +01:00
|
|
|
if self.state == "review":
|
2021-04-06 02:14:11 +02:00
|
|
|
dirty = self.reviewer.op_executed(changes, handler, focused)
|
2021-03-14 13:08:37 +01:00
|
|
|
elif self.state == "overview":
|
2021-04-06 02:14:11 +02:00
|
|
|
dirty = self.overview.op_executed(changes, handler, focused)
|
2021-03-14 13:08:37 +01:00
|
|
|
elif self.state == "deckBrowser":
|
2021-04-06 02:14:11 +02:00
|
|
|
dirty = self.deckBrowser.op_executed(changes, handler, focused)
|
2021-03-14 15:03:41 +01:00
|
|
|
else:
|
|
|
|
dirty = False
|
|
|
|
|
|
|
|
if not focused and dirty:
|
|
|
|
self.fade_out_webview()
|
2021-03-14 13:08:37 +01:00
|
|
|
|
2021-05-28 03:09:16 +02:00
|
|
|
if changes.mtime:
|
|
|
|
self.toolbar.update_sync_status()
|
|
|
|
|
2021-03-14 13:08:37 +01:00
|
|
|
def on_focus_did_change(
|
2021-10-03 10:59:42 +02:00
|
|
|
self, new_focus: QWidget | None, _old: QWidget | None
|
2021-03-14 13:08:37 +01:00
|
|
|
) -> None:
|
|
|
|
"If main window has received focus, ensure current UI state is updated."
|
2021-06-01 07:16:53 +02:00
|
|
|
if new_focus and new_focus.window() == self:
|
2021-03-14 13:08:37 +01:00
|
|
|
if self.state == "review":
|
|
|
|
self.reviewer.refresh_if_needed()
|
|
|
|
elif self.state == "overview":
|
|
|
|
self.overview.refresh_if_needed()
|
|
|
|
elif self.state == "deckBrowser":
|
|
|
|
self.deckBrowser.refresh_if_needed()
|
|
|
|
|
2021-03-14 15:03:41 +01:00
|
|
|
def fade_out_webview(self) -> None:
|
|
|
|
self.web.eval("document.body.style.opacity = 0.3")
|
|
|
|
|
|
|
|
def fade_in_webview(self) -> None:
|
|
|
|
self.web.eval("document.body.style.opacity = 1")
|
|
|
|
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
def reset(self, unused_arg: bool = False) -> None:
|
|
|
|
"""Legacy method of telling UI to refresh after changes made to DB.
|
|
|
|
|
2021-04-06 09:07:38 +02:00
|
|
|
New code should use CollectionOp() instead."""
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.col:
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
# fire new `operation_did_execute` hook first. If the overview
|
|
|
|
# or review screen are currently open, they will rebuild the study
|
|
|
|
# queues (via mw.col.reset())
|
|
|
|
self._synthesize_op_did_execute_from_reset()
|
|
|
|
# fire the old reset hook
|
2020-01-15 07:53:24 +01:00
|
|
|
gui_hooks.state_did_reset()
|
2021-03-05 13:45:55 +01:00
|
|
|
self.update_undo_actions()
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
|
2021-03-14 13:08:37 +01:00
|
|
|
# legacy
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def requireReset(
|
|
|
|
self,
|
|
|
|
modal: bool = False,
|
2021-03-14 13:08:37 +01:00
|
|
|
reason: Any = None,
|
2021-02-01 11:59:18 +01:00
|
|
|
context: Any = None,
|
|
|
|
) -> None:
|
2021-05-08 08:56:51 +02:00
|
|
|
traceback.print_stack(file=sys.stdout)
|
|
|
|
print("requireReset() is obsolete; please use CollectionOp()")
|
2021-03-14 13:08:37 +01:00
|
|
|
self.reset()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def maybeReset(self) -> None:
|
2021-03-14 13:08:37 +01:00
|
|
|
pass
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def delayedMaybeReset(self) -> None:
|
2021-03-14 13:08:37 +01:00
|
|
|
pass
|
2012-12-21 08:51:59 +01:00
|
|
|
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
def _resetRequiredState(self, oldState: MainWindowState) -> None:
|
2021-03-14 13:08:37 +01:00
|
|
|
pass
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# HTML helpers
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
def button(
|
|
|
|
self,
|
|
|
|
link: str,
|
|
|
|
name: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
key: str | None = None,
|
2019-12-23 01:34:10 +01:00
|
|
|
class_: str = "",
|
|
|
|
id: str = "",
|
|
|
|
extra: str = "",
|
|
|
|
) -> str:
|
2021-02-11 01:09:06 +01:00
|
|
|
class_ = f"but {class_}"
|
2012-12-21 08:51:59 +01:00
|
|
|
if key:
|
2021-03-26 05:21:04 +01:00
|
|
|
key = tr.actions_shortcut_key(val=key)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
|
|
|
key = ""
|
2019-12-23 01:34:10 +01:00
|
|
|
return """
|
2021-10-03 10:59:42 +02:00
|
|
|
<button id="{}" class="{}" onclick="pycmd('{}');return false;"
|
|
|
|
title="{}" {}>{}</button>""".format(
|
2019-12-23 01:34:10 +01:00
|
|
|
id,
|
|
|
|
class_,
|
|
|
|
link,
|
|
|
|
key,
|
|
|
|
extra,
|
|
|
|
name,
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Main window setup
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupMainWindow(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
# main window
|
|
|
|
self.form = aqt.forms.main.Ui_MainWindow()
|
|
|
|
self.form.setupUi(self)
|
|
|
|
# toolbar
|
2021-12-20 11:22:21 +01:00
|
|
|
tweb = self.toolbarWeb = AnkiWebView(title="top toolbar")
|
2021-10-05 05:53:01 +02:00
|
|
|
tweb.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
|
2022-01-18 10:12:57 +01:00
|
|
|
tweb.disable_zoom()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.toolbar = aqt.toolbar.Toolbar(self, tweb)
|
|
|
|
# main area
|
2021-12-20 11:22:21 +01:00
|
|
|
self.web = MainWebView(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
# bottom area
|
2021-12-20 11:22:21 +01:00
|
|
|
sweb = self.bottomWeb = AnkiWebView(title="bottom toolbar")
|
2021-10-05 05:53:01 +02:00
|
|
|
sweb.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
|
2022-01-18 10:12:57 +01:00
|
|
|
sweb.disable_zoom()
|
2012-12-21 08:51:59 +01:00
|
|
|
# add in a layout
|
|
|
|
self.mainLayout = QVBoxLayout()
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mainLayout.setContentsMargins(0, 0, 0, 0)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mainLayout.setSpacing(0)
|
|
|
|
self.mainLayout.addWidget(tweb)
|
|
|
|
self.mainLayout.addWidget(self.web)
|
|
|
|
self.mainLayout.addWidget(sweb)
|
|
|
|
self.form.centralwidget.setLayout(self.mainLayout)
|
|
|
|
|
2019-04-09 10:48:50 +02:00
|
|
|
# force webengine processes to load before cwd is changed
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_win:
|
2021-03-17 05:51:59 +01:00
|
|
|
for webview in self.web, self.bottomWeb:
|
|
|
|
webview.force_load_hack()
|
2019-04-09 10:48:50 +02:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def closeAllWindows(self, onsuccess: Callable) -> None:
|
2017-08-16 11:45:39 +02:00
|
|
|
aqt.dialogs.closeAll(onsuccess)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Components
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupSignals(self) -> None:
|
2020-07-18 03:26:04 +02:00
|
|
|
signal.signal(signal.SIGINT, self.onUnixSignal)
|
|
|
|
signal.signal(signal.SIGTERM, self.onUnixSignal)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def onUnixSignal(self, signum: Any, frame: Any) -> None:
|
2020-03-02 11:50:17 +01:00
|
|
|
# schedule a rollback & quit
|
2021-02-01 14:28:21 +01:00
|
|
|
def quit() -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.col.db.rollback()
|
|
|
|
self.close()
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2022-02-24 12:15:56 +01:00
|
|
|
self.progress.single_shot(100, quit)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupProgress(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.progress = aqt.progress.ProgressManager(self)
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupErrorHandler(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
import aqt.errors
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
self.errorHandler = aqt.errors.ErrorHandler(self)
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def setupAddons(self, args: list | None) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
import aqt.addons
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
self.addonManager = aqt.addons.AddonManager(self)
|
2020-01-04 04:34:16 +01:00
|
|
|
|
|
|
|
if args and args[0] and self._isAddon(args[0]):
|
|
|
|
self.installAddon(args[0], startup=True)
|
|
|
|
|
2017-08-28 12:51:43 +02:00
|
|
|
if not self.safeMode:
|
|
|
|
self.addonManager.loadAddons()
|
2020-01-19 02:31:09 +01:00
|
|
|
self.maybe_check_for_addon_updates()
|
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def maybe_check_for_addon_updates(self) -> None:
|
2020-01-19 02:31:09 +01:00
|
|
|
last_check = self.pm.last_addon_update_check()
|
2021-10-25 06:50:13 +02:00
|
|
|
elap = int_time() - last_check
|
2020-01-19 02:31:09 +01:00
|
|
|
|
2022-01-21 12:43:54 +01:00
|
|
|
if elap > 86_400 or self.pm.last_run_version() != point_version():
|
2020-01-19 02:31:09 +01:00
|
|
|
check_and_prompt_for_updates(
|
2021-03-09 14:27:28 +01:00
|
|
|
self,
|
|
|
|
self.addonManager,
|
|
|
|
self.on_updates_installed,
|
|
|
|
requested_by_user=False,
|
2020-01-19 02:31:09 +01:00
|
|
|
)
|
2021-10-25 06:50:13 +02:00
|
|
|
self.pm.set_last_addon_update_check(int_time())
|
2020-01-19 02:31:09 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def on_updates_installed(self, log: list[DownloadLogEntry]) -> None:
|
2020-01-19 02:31:09 +01:00
|
|
|
if log:
|
|
|
|
show_log_to_user(self, log)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupSpellCheck(self) -> None:
|
2019-12-23 01:34:10 +01:00
|
|
|
os.environ["QTWEBENGINE_DICTIONARIES_PATH"] = os.path.join(
|
|
|
|
self.pm.base, "dictionaries"
|
|
|
|
)
|
2019-03-06 14:18:26 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupThreads(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self._mainThread = QThread.currentThread()
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
self._background_op_count = 0
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def inMainThread(self) -> bool:
|
2012-12-21 08:51:59 +01:00
|
|
|
return self._mainThread == QThread.currentThread()
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupDeckBrowser(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.deckbrowser import DeckBrowser
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
self.deckBrowser = DeckBrowser(self)
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupOverview(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.overview import Overview
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
self.overview = Overview(self)
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupReviewer(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.reviewer import Reviewer
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
self.reviewer = Reviewer(self)
|
|
|
|
|
|
|
|
# Syncing
|
|
|
|
##########################################################################
|
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def on_sync_button_clicked(self) -> None:
|
2020-02-04 02:41:20 +01:00
|
|
|
if self.media_syncer.is_syncing():
|
2020-02-04 03:26:10 +01:00
|
|
|
self.media_syncer.show_sync_log()
|
2020-02-04 02:41:20 +01:00
|
|
|
else:
|
2020-05-31 02:53:54 +02:00
|
|
|
auth = self.pm.sync_auth()
|
|
|
|
if not auth:
|
2020-11-30 01:14:43 +01:00
|
|
|
sync_login(
|
|
|
|
self,
|
2020-12-01 01:20:55 +01:00
|
|
|
lambda: self._sync_collection_and_media(self._refresh_after_sync),
|
2020-11-30 01:14:43 +01:00
|
|
|
)
|
2020-05-31 02:53:54 +02:00
|
|
|
else:
|
2020-12-01 01:20:55 +01:00
|
|
|
self._sync_collection_and_media(self._refresh_after_sync)
|
2020-11-30 01:14:43 +01:00
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def _refresh_after_sync(self) -> None:
|
2020-11-30 01:14:43 +01:00
|
|
|
self.toolbar.redraw()
|
2017-08-16 06:38:55 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def _sync_collection_and_media(self, after_sync: Callable[[], None]) -> None:
|
2020-05-31 02:53:54 +02:00
|
|
|
"Caller should ensure auth available."
|
|
|
|
# start media sync if not already running
|
|
|
|
if not self.media_syncer.is_syncing():
|
|
|
|
self.media_syncer.start()
|
2017-08-16 06:38:55 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def on_collection_sync_finished() -> None:
|
2021-03-04 10:17:19 +01:00
|
|
|
self.col.clear_python_undo()
|
2020-08-20 07:35:23 +02:00
|
|
|
self.col.models._clear_cache()
|
2020-09-14 12:22:01 +02:00
|
|
|
gui_hooks.sync_did_finish()
|
2020-09-15 13:06:11 +02:00
|
|
|
self.reset()
|
2017-08-16 06:38:55 +02:00
|
|
|
|
2020-09-14 13:06:20 +02:00
|
|
|
after_sync()
|
|
|
|
|
2020-09-14 12:22:01 +02:00
|
|
|
gui_hooks.sync_will_start()
|
2020-05-31 02:53:54 +02:00
|
|
|
sync_collection(self, on_done=on_collection_sync_finished)
|
|
|
|
|
|
|
|
def maybe_auto_sync_on_open_close(self, after_sync: Callable[[], None]) -> None:
|
|
|
|
"If disabled, after_sync() is called immediately."
|
|
|
|
if self.can_auto_sync():
|
|
|
|
self._sync_collection_and_media(after_sync)
|
|
|
|
else:
|
|
|
|
after_sync()
|
2017-08-16 06:38:55 +02:00
|
|
|
|
2020-02-05 02:55:46 +01:00
|
|
|
def maybe_auto_sync_media(self) -> None:
|
2020-05-31 02:53:54 +02:00
|
|
|
if self.can_auto_sync():
|
2020-02-05 02:55:46 +01:00
|
|
|
return
|
2020-05-31 02:53:54 +02:00
|
|
|
# media_syncer takes care of media syncing preference check
|
2020-02-05 02:55:46 +01:00
|
|
|
self.media_syncer.start()
|
|
|
|
|
2020-05-31 02:53:54 +02:00
|
|
|
def can_auto_sync(self) -> bool:
|
2020-05-31 03:24:33 +02:00
|
|
|
return (
|
|
|
|
self.pm.auto_syncing_enabled()
|
2020-05-31 03:49:05 +02:00
|
|
|
and bool(self.pm.sync_auth())
|
2020-05-31 02:53:54 +02:00
|
|
|
and not self.safeMode
|
Backups (#1685)
* Add zstd dep
* Implement backend backup with zstd
* Implement backup thinning
* Write backup meta
* Use new file ending anki21b
* Asynchronously backup on collection close in Rust
* Revert "Add zstd dep"
This reverts commit 3fcb2141d2be15f907269d13275c41971431385c.
* Add zstd again
* Take backup col path from col struct
* Fix formatting
* Implement backup restoring on backend
* Normalize restored media file names
* Refactor `extract_legacy_data()`
A bit cumbersome due to borrowing rules.
* Refactor
* Make thinning calendar-based and gradual
* Consider last kept backups of previous stages
* Import full apkgs and colpkgs with backend
* Expose new backup settings
* Test `BackupThinner` and make it deterministic
* Mark backup_path when closing optional
* Delete leaky timer
* Add progress updates for restoring media
* Write restored collection to tempfile first
* Do collection compression in the background thread
This has us currently storing an uncompressed and compressed copy of
the collection in memory (not ideal), but means the collection can be
closed without waiting for compression to complete. On a large collection,
this takes a close and reopen from about 0.55s to about 0.07s. The old
backup code for comparison: about 0.35s for compression off, about
8.5s for zip compression.
* Use multithreading in zstd compression
On my system, this reduces the compression time of a large collection
from about 0.55s to 0.08s.
* Stream compressed collection data into zip file
* Tweak backup explanation
+ Fix incorrect tab order for ignore accents option
* Decouple restoring backup and full import
In the first case, no profile is opened, unless the new collection
succeeds to load.
In the second case, either the old collection is reloaded or the new one
is loaded.
* Fix number gap in Progress message
* Don't revert backup when media fails but report it
* Tweak error flow
* Remove native BackupLimits enum
* Fix type annotation
* Add thinning test for whole year
* Satisfy linter
* Await async backup to finish
* Move restart disclaimer out of backup tab
Should be visible regardless of the current tab.
* Write restored collection in chunks
* Refactor
* Write media in chunks and refactor
* Log error if removing file fails
* join_backup_task -> await_backup_completion
* Refactor backup.rs
* Refactor backup meta and collection extraction
* Fix wrong error being returned
* Call sync_all() on new collection
* Add ImportError
* Store logger in Backend, instead of creating one on demand
init_backend() accepts a Logger rather than a log file, to allow other
callers to customize the logger if they wish.
In the future we may want to explore using the tracing crate as an
alternative; it's a bit more ergonomic, as a logger doesn't need to be
passed around, and it plays more nicely with async code.
* Sync file contents prior to rename; sync folder after rename.
* Limit backup creation to once per 30 min
* Use zstd::stream::copy_decode
* Make importing abortable
* Don't revert if backup media is aborted
* Set throttle implicitly
* Change force flag to minimum_backup_interval
* Don't attempt to open folders on Windows
* Join last backup thread before starting new one
Also refactor.
* Disable auto sync and backup when restoring again
* Force backup on full download
* Include the reason why a media file import failed, and the file path
- Introduce a FileIoError that contains a string representation of
the underlying I/O error, and an associated path. There are a few
places in the code where we're currently manually including the filename
in a custom error message, and this is a step towards a more consistent
approach (but we may be better served with a more general approach in
the future similar to Anyhow's .context())
- Move the error message into importing.ftl, as it's a bit neater
when error messages live in the same file as the rest of the messages
associated with some functionality.
* Fix importing of media files
* Minor wording tweaks
* Save an allocation
I18n strings with replacements are already strings, so we can skip the
extra allocation. Not that it matters here at all.
* Terminate import if file missing from archive
If a third-party tool is creating invalid archives, the user should know
about it. This should be rare, so I did not attempt to make it
translatable.
* Skip multithreaded compression on small collections
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
|
|
|
and not self.restoring_backup
|
2020-05-31 03:24:33 +02:00
|
|
|
)
|
2020-05-30 04:28:22 +02:00
|
|
|
|
2020-05-31 02:53:54 +02:00
|
|
|
# legacy
|
2021-02-01 14:28:21 +01:00
|
|
|
def _sync(self) -> None:
|
2020-05-31 02:53:54 +02:00
|
|
|
pass
|
2020-05-31 03:24:33 +02:00
|
|
|
|
2020-05-31 02:53:54 +02:00
|
|
|
onSync = on_sync_button_clicked
|
2020-05-30 04:28:22 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Tools
|
|
|
|
##########################################################################
|
|
|
|
|
2020-08-01 04:27:54 +02:00
|
|
|
def raiseMain(self) -> bool:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.app.activeWindow():
|
|
|
|
# make sure window is shown
|
2021-10-05 05:53:01 +02:00
|
|
|
self.setWindowState(self.windowState() & ~Qt.WindowState.WindowMinimized) # type: ignore
|
2012-12-21 08:51:59 +01:00
|
|
|
return True
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupStyle(self) -> None:
|
2021-11-24 22:17:41 +01:00
|
|
|
theme_manager.apply_style()
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_lin:
|
2021-11-24 23:41:15 +01:00
|
|
|
# On Linux, the check requires invoking an external binary,
|
|
|
|
# which we don't want to be doing frequently
|
|
|
|
interval_secs = 300
|
|
|
|
else:
|
|
|
|
interval_secs = 5
|
2021-11-24 22:17:41 +01:00
|
|
|
self.progress.timer(
|
2021-11-24 23:41:15 +01:00
|
|
|
interval_secs * 1000,
|
|
|
|
theme_manager.apply_style_if_system_style_changed,
|
|
|
|
True,
|
|
|
|
False,
|
2022-02-18 10:00:12 +01:00
|
|
|
parent=self,
|
2021-11-24 22:17:41 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
def set_theme(self, theme: Theme) -> None:
|
|
|
|
self.pm.set_theme(theme)
|
|
|
|
self.setupStyle()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Key handling
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupKeys(self) -> None:
|
2017-06-22 08:36:54 +02:00
|
|
|
globalShortcuts = [
|
2018-03-18 15:50:50 +01:00
|
|
|
("Ctrl+:", self.onDebug),
|
2017-06-22 08:36:54 +02:00
|
|
|
("d", lambda: self.moveToState("deckBrowser")),
|
|
|
|
("s", self.onStudyKey),
|
|
|
|
("a", self.onAddCard),
|
|
|
|
("b", self.onBrowse),
|
2018-05-31 05:05:30 +02:00
|
|
|
("t", self.onStats),
|
2020-05-31 02:53:54 +02:00
|
|
|
("y", self.on_sync_button_clicked),
|
2017-06-22 08:36:54 +02:00
|
|
|
]
|
|
|
|
self.applyShortcuts(globalShortcuts)
|
2021-10-03 10:59:42 +02:00
|
|
|
self.stateShortcuts: list[QShortcut] = []
|
2017-06-22 08:36:54 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
def applyShortcuts(
|
2021-10-03 10:59:42 +02:00
|
|
|
self, shortcuts: Sequence[tuple[str, Callable]]
|
|
|
|
) -> list[QShortcut]:
|
2017-06-22 08:36:54 +02:00
|
|
|
qshortcuts = []
|
|
|
|
for key, fn in shortcuts:
|
2019-12-23 01:34:10 +01:00
|
|
|
scut = QShortcut(QKeySequence(key), self, activated=fn) # type: ignore
|
2018-08-29 02:07:33 +02:00
|
|
|
scut.setAutoRepeat(False)
|
|
|
|
qshortcuts.append(scut)
|
2017-06-22 08:36:54 +02:00
|
|
|
return qshortcuts
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def setStateShortcuts(self, shortcuts: list[tuple[str, Callable]]) -> None:
|
2020-01-15 07:53:24 +01:00
|
|
|
gui_hooks.state_shortcuts_will_change(self.state, shortcuts)
|
2020-01-15 03:46:53 +01:00
|
|
|
# legacy hook
|
2021-02-11 01:09:06 +01:00
|
|
|
runHook(f"{self.state}StateShortcuts", shortcuts)
|
2017-06-22 08:36:54 +02:00
|
|
|
self.stateShortcuts = self.applyShortcuts(shortcuts)
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def clearStateShortcuts(self) -> None:
|
2017-06-22 08:36:54 +02:00
|
|
|
for qs in self.stateShortcuts:
|
2021-10-07 07:36:50 +02:00
|
|
|
sip.delete(qs) # type: ignore
|
2017-06-22 08:36:54 +02:00
|
|
|
self.stateShortcuts = []
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def onStudyKey(self) -> None:
|
2017-06-22 08:36:54 +02:00
|
|
|
if self.state == "overview":
|
|
|
|
self.col.startTimebox()
|
|
|
|
self.moveToState("review")
|
2017-06-06 07:56:21 +02:00
|
|
|
else:
|
2017-06-22 08:36:54 +02:00
|
|
|
self.moveToState("overview")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# App exit
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def closeEvent(self, event: QCloseEvent) -> None:
|
2022-01-21 12:43:54 +01:00
|
|
|
self.pm.set_last_run_version()
|
2017-08-16 11:45:39 +02:00
|
|
|
if self.state == "profileManager":
|
|
|
|
# if profile manager active, this event may fire via OS X menu bar's
|
|
|
|
# quit option
|
|
|
|
self.profileDiag.close()
|
|
|
|
event.accept()
|
|
|
|
else:
|
|
|
|
# ignore the event for now, as we need time to clean up
|
|
|
|
event.ignore()
|
|
|
|
self.unloadProfileAndExit()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Undo & autosave
|
|
|
|
##########################################################################
|
|
|
|
|
2021-04-03 06:38:49 +02:00
|
|
|
def undo(self) -> None:
|
2021-05-19 07:18:39 +02:00
|
|
|
"Call operations/collection.py:undo() directly instead."
|
2021-04-06 06:36:13 +02:00
|
|
|
undo(parent=self)
|
2021-03-04 10:17:19 +01:00
|
|
|
|
2021-05-19 07:18:39 +02:00
|
|
|
def redo(self) -> None:
|
|
|
|
"Call operations/collection.py:redo() directly instead."
|
|
|
|
redo(parent=self)
|
|
|
|
|
|
|
|
def undo_actions_info(self) -> UndoActionsInfo:
|
|
|
|
"Info about the current undo/redo state for updating menus."
|
|
|
|
status = self.col.undo_status() if self.col else UndoStatus()
|
|
|
|
return UndoActionsInfo.from_undo_status(status)
|
|
|
|
|
|
|
|
def update_undo_actions(self) -> None:
|
|
|
|
"""Tell the UI to redraw the undo/redo menu actions based on the current state.
|
|
|
|
|
|
|
|
Usually you do not need to call this directly; it is called when a
|
|
|
|
CollectionOp is run, and will be called when the legacy .reset() or
|
|
|
|
.checkpoint() methods are used."""
|
|
|
|
info = self.undo_actions_info()
|
|
|
|
self.form.actionUndo.setText(info.undo_text)
|
|
|
|
self.form.actionUndo.setEnabled(info.can_undo)
|
|
|
|
self.form.actionRedo.setText(info.redo_text)
|
|
|
|
self.form.actionRedo.setEnabled(info.can_redo)
|
|
|
|
self.form.actionRedo.setVisible(info.show_redo)
|
|
|
|
gui_hooks.undo_state_did_change(info)
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def checkpoint(self, name: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.col.save(name)
|
2021-03-05 13:45:55 +01:00
|
|
|
self.update_undo_actions()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def autosave(self) -> None:
|
2021-03-05 04:07:52 +01:00
|
|
|
self.col.autosave()
|
2021-03-05 13:45:55 +01:00
|
|
|
self.update_undo_actions()
|
|
|
|
|
|
|
|
maybeEnableUndo = update_undo_actions
|
2021-03-12 08:54:56 +01:00
|
|
|
onUndo = undo
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Other menu operations
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def onAddCard(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
aqt.dialogs.open("AddCards", self)
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def onBrowse(self) -> None:
|
2021-02-01 11:54:28 +01:00
|
|
|
aqt.dialogs.open("Browser", self, card=self.reviewer.card)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def onEditCurrent(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
aqt.dialogs.open("EditCurrent", self)
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onOverview(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.col.reset()
|
|
|
|
self.moveToState("overview")
|
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def onStats(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
deck = self._selectedDeck()
|
|
|
|
if not deck:
|
|
|
|
return
|
2021-03-17 05:51:59 +01:00
|
|
|
want_old = KeyboardModifiersPressed().shift
|
2020-06-30 09:08:10 +02:00
|
|
|
if want_old:
|
|
|
|
aqt.dialogs.open("DeckStats", self)
|
|
|
|
else:
|
|
|
|
aqt.dialogs.open("NewDeckStats", self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onPrefs(self) -> None:
|
2017-09-10 07:15:12 +02:00
|
|
|
aqt.dialogs.open("Preferences", self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def onNoteTypes(self) -> None:
|
2012-12-22 00:21:24 +01:00
|
|
|
import aqt.models
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-22 00:21:24 +01:00
|
|
|
aqt.models.Models(self, self, fromMain=True)
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onAbout(self) -> None:
|
2017-06-26 05:05:11 +02:00
|
|
|
aqt.dialogs.open("About", self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onDonate(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
openLink(aqt.appDonate)
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onDocumentation(self) -> None:
|
2021-01-25 14:45:47 +01:00
|
|
|
openHelp(HelpPage.INDEX)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-05-27 05:11:20 +02:00
|
|
|
# legacy
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def onDeckConf(self, deck: DeckDict | None = None) -> None:
|
2021-05-27 05:11:20 +02:00
|
|
|
pass
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Importing & exporting
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def handleImport(self, path: str) -> None:
|
2013-06-30 00:08:37 +02:00
|
|
|
import aqt.importing
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2013-06-30 00:08:37 +02:00
|
|
|
if not os.path.exists(path):
|
2021-03-26 04:48:26 +01:00
|
|
|
showInfo(tr.qt_misc_please_use_fileimport_to_import_this())
|
2019-12-20 09:43:52 +01:00
|
|
|
return None
|
2013-06-30 00:08:37 +02:00
|
|
|
|
|
|
|
aqt.importing.importFile(self, path)
|
2019-12-20 09:43:52 +01:00
|
|
|
return None
|
2013-06-30 00:08:37 +02:00
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def onImport(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
import aqt.importing
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
aqt.importing.onImport(self)
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def onExport(self, did: DeckId | None = None) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
import aqt.exporting
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2014-06-20 02:13:12 +02:00
|
|
|
aqt.exporting.ExportDialog(self, did=did)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-01-03 17:57:33 +01:00
|
|
|
# Installing add-ons from CLI / mimetype handler
|
|
|
|
##########################################################################
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def installAddon(self, path: str, startup: bool = False) -> None:
|
2020-01-03 17:57:33 +01:00
|
|
|
from aqt.addons import installAddonPackages
|
2020-01-03 18:23:28 +01:00
|
|
|
|
2020-01-04 04:34:16 +01:00
|
|
|
installAddonPackages(
|
2020-01-04 04:45:43 +01:00
|
|
|
self.addonManager,
|
|
|
|
[path],
|
|
|
|
warn=True,
|
|
|
|
advise_restart=not startup,
|
|
|
|
strictly_modal=startup,
|
|
|
|
parent=None if startup else self,
|
2020-01-04 04:34:16 +01:00
|
|
|
)
|
2020-01-03 17:57:33 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Cramming
|
|
|
|
##########################################################################
|
|
|
|
|
2021-02-01 23:33:41 +01:00
|
|
|
def onCram(self) -> None:
|
2021-03-24 04:17:12 +01:00
|
|
|
aqt.dialogs.open("FilteredDeckConfigDialog", self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Menu, title bar & status
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupMenus(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
m = self.form
|
2022-02-17 07:31:46 +01:00
|
|
|
|
|
|
|
# File
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(
|
|
|
|
m.actionSwitchProfile.triggered, self.unloadProfileAndShowProfileManager
|
|
|
|
)
|
|
|
|
qconnect(m.actionImport.triggered, self.onImport)
|
|
|
|
qconnect(m.actionExport.triggered, self.onExport)
|
Backup improvements (#1728)
* Collection needs to be closed prior to backup even when not downgrading
* Backups -> BackupLimits
* Some improvements to backup_task
- backup_inner now returns the error instead of logging it, so that
the frontend can discover the issue when they await a backup (or create
another one)
- start_backup() was acquiring backup_task twice, and if another thread
started a backup between the two locks, the task could have been accidentally
overwritten without awaiting it
* Backups no longer require a collection close
- Instead of closing the collection, we ensure there is no active
transaction, and flush the WAL to disk. This means the undo history
is no longer lost on backup, which will be particularly useful if we
add a periodic backup in the future.
- Because a close is no longer required, backups are now achieved with
a separate command, instead of being included in CloseCollection().
- Full sync no longer requires an extra close+reopen step, and we now
wait for the backup to complete before proceeding.
- Create a backup before 'check db'
* Add File>Create Backup
https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157
* Defer checkpoint until we know we need it
When running periodic backups on a timer, we don't want to be fsync()ing
unnecessarily.
* Skip backup if modification time has not changed
We don't want the user leaving Anki open overnight, and coming back
to lots of identical backups.
* Periodic backups
Creates an automatic backup every 30 minutes if the collection has been
modified.
If there's a legacy checkpoint active, tries again 5 minutes later.
* Switch to a user-configurable backup duration
CreateBackup() now uses a simple force argument to determine whether
the user's limits should be respected or not, and only potentially
destructive ops (full download, check DB) override the user's configured
limit.
I considered having a separate limit for collection close and automatic
backups (eg keeping the previous 5 minute limit for collection close),
but that had two downsides:
- When the user closes their collection at the end of the day, they'd
get a recent backup. When they open the collection the next day, it
would get backed up again within 5 minutes, even though not much had
changed.
- Multiple limits are harder to communicate to users in the UI
Some remaining decisions I wasn't 100% sure about:
- If force is true but the collection has not been modified, the backup
will be skipped. If the user manually deleted their backups without
closing Anki, they wouldn't get a new one if the mtime hadn't changed.
- Force takes preference over the configured backup interval - should
we be ignored the user here, or take no backups at all?
Did a sneaky edit of the existing ftl string, as it hasn't been live
long.
* Move maybe_backup() into Collection
* Use a single method for manual and periodic backups
When manually creating a backup via the File menu, we no longer make
the user wait until the backup completes. As we continue waiting for
the backup in the background, if any errors occur, the user will get
notified about it fairly quickly.
* Show message to user if backup was skipped due to no changes
+ Don't incorrectly assert a backup will be created on force
* Add "automatic" to description
* Ensure we backup prior to importing colpkg if collection open
The backup doesn't happen when invoked from 'open backup' in the profile
screen, which matches Anki's previous behaviour. The user could
potentially clobber up to 30 minutes of their work if they exited to
the profile screen and restored a backup, but the alternative is we
create backups every time a backup is restored, which may happen a number
of times if the user is trying various ones. Or we could go back to a
separate throttle amount for this case, at the cost of more complexity.
* Remove the 0 special case on backup interval; minimum of 5 minutes
https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
|
|
|
qconnect(m.action_create_backup.triggered, self.on_create_backup_now)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(m.actionExit.triggered, self.close)
|
2022-02-17 07:31:46 +01:00
|
|
|
|
|
|
|
# Help
|
|
|
|
qconnect(m.actionDocumentation.triggered, self.onDocumentation)
|
|
|
|
qconnect(m.actionDonate.triggered, self.onDonate)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(m.actionAbout.triggered, self.onAbout)
|
2022-02-17 07:31:46 +01:00
|
|
|
|
|
|
|
# Edit
|
2021-05-19 07:18:39 +02:00
|
|
|
qconnect(m.actionUndo.triggered, self.undo)
|
|
|
|
qconnect(m.actionRedo.triggered, self.redo)
|
2022-02-17 07:31:46 +01:00
|
|
|
|
|
|
|
# Tools
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(m.actionFullDatabaseCheck.triggered, self.onCheckDB)
|
|
|
|
qconnect(m.actionCheckMediaDatabase.triggered, self.on_check_media_db)
|
|
|
|
qconnect(m.actionStudyDeck.triggered, self.onStudyDeck)
|
|
|
|
qconnect(m.actionCreateFiltered.triggered, self.onCram)
|
|
|
|
qconnect(m.actionEmptyCards.triggered, self.onEmptyCards)
|
|
|
|
qconnect(m.actionNoteTypes.triggered, self.onNoteTypes)
|
2022-02-17 07:31:46 +01:00
|
|
|
qconnect(m.actionPreferences.triggered, self.onPrefs)
|
|
|
|
|
|
|
|
# View
|
|
|
|
qconnect(
|
|
|
|
m.actionZoomIn.triggered,
|
|
|
|
lambda: self.web.setZoomFactor(self.web.zoomFactor() + 0.1),
|
|
|
|
)
|
|
|
|
qconnect(
|
|
|
|
m.actionZoomOut.triggered,
|
|
|
|
lambda: self.web.setZoomFactor(self.web.zoomFactor() - 0.1),
|
|
|
|
)
|
|
|
|
qconnect(m.actionResetZoom.triggered, lambda: self.web.setZoomFactor(1))
|
|
|
|
# app-wide shortcut
|
|
|
|
qconnect(m.actionFullScreen.triggered, self.on_toggle_full_screen)
|
|
|
|
m.actionFullScreen.setShortcut(
|
|
|
|
QKeySequence("F11") if is_lin else QKeySequence.StandardKey.FullScreen
|
|
|
|
)
|
|
|
|
m.actionFullScreen.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def updateTitleBar(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setWindowTitle("Anki")
|
|
|
|
|
2022-02-17 07:31:46 +01:00
|
|
|
# View
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
def on_toggle_full_screen(self) -> None:
|
2022-03-09 05:21:54 +01:00
|
|
|
if disallow_full_screen():
|
|
|
|
showWarning(
|
|
|
|
tr.actions_currently_unsupported(),
|
|
|
|
parent=self,
|
|
|
|
help=HelpPage.FULL_SCREEN_ISSUE,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
window = self.app.activeWindow()
|
|
|
|
window.setWindowState(
|
|
|
|
window.windowState() ^ Qt.WindowState.WindowFullScreen
|
|
|
|
)
|
2022-02-17 07:31:46 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Auto update
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupAutoUpdate(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
import aqt.update
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
self.autoUpdate = aqt.update.LatestVersionFinder(self)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(self.autoUpdate.newVerAvail, self.newVerAvail)
|
|
|
|
qconnect(self.autoUpdate.newMsg, self.newMsg)
|
|
|
|
qconnect(self.autoUpdate.clockIsOff, self.clockIsOff)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.autoUpdate.start()
|
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def newVerAvail(self, ver: str) -> None:
|
2019-12-23 01:34:10 +01:00
|
|
|
if self.pm.meta.get("suppressUpdate", None) != ver:
|
2012-12-21 08:51:59 +01:00
|
|
|
aqt.update.askAndUpdate(self, ver)
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def newMsg(self, data: dict) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
aqt.update.showMessages(self, data)
|
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def clockIsOff(self, diff: int) -> None:
|
2021-11-25 00:06:16 +01:00
|
|
|
if dev_mode:
|
2020-11-15 09:29:16 +01:00
|
|
|
print("clock is off; ignoring")
|
|
|
|
return
|
2021-03-26 05:21:04 +01:00
|
|
|
diffText = tr.qt_misc_second(count=diff)
|
|
|
|
warn = tr.qt_misc_in_order_to_ensure_your_collection(val="%s") % diffText
|
2013-10-20 03:26:11 +02:00
|
|
|
showWarning(warn)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.app.closeAllWindows()
|
|
|
|
|
2020-02-05 03:38:36 +01:00
|
|
|
# Timers
|
2012-12-21 08:51:59 +01:00
|
|
|
##########################################################################
|
|
|
|
|
2020-02-05 03:38:36 +01:00
|
|
|
def setup_timers(self) -> None:
|
|
|
|
# refresh decks every 10 minutes
|
2022-02-18 10:00:12 +01:00
|
|
|
self.progress.timer(10 * 60 * 1000, self.onRefreshTimer, True, parent=self)
|
2020-02-05 03:38:36 +01:00
|
|
|
# check media sync every 5 minutes
|
2022-02-18 10:00:12 +01:00
|
|
|
self.progress.timer(5 * 60 * 1000, self.on_autosync_timer, True, parent=self)
|
2021-03-05 04:07:52 +01:00
|
|
|
# periodic garbage collection
|
2022-02-18 10:00:12 +01:00
|
|
|
self.progress.timer(
|
2022-02-24 12:15:56 +01:00
|
|
|
15 * 60 * 1000, self.garbage_collect_now, True, False, parent=self
|
2022-02-18 10:00:12 +01:00
|
|
|
)
|
2020-07-18 03:26:04 +02:00
|
|
|
# ensure Python interpreter runs at least once per second, so that
|
|
|
|
# SIGINT/SIGTERM is processed without a long delay
|
2022-02-18 10:00:12 +01:00
|
|
|
self.progress.timer(1000, lambda: None, True, False, parent=self)
|
Backup improvements (#1728)
* Collection needs to be closed prior to backup even when not downgrading
* Backups -> BackupLimits
* Some improvements to backup_task
- backup_inner now returns the error instead of logging it, so that
the frontend can discover the issue when they await a backup (or create
another one)
- start_backup() was acquiring backup_task twice, and if another thread
started a backup between the two locks, the task could have been accidentally
overwritten without awaiting it
* Backups no longer require a collection close
- Instead of closing the collection, we ensure there is no active
transaction, and flush the WAL to disk. This means the undo history
is no longer lost on backup, which will be particularly useful if we
add a periodic backup in the future.
- Because a close is no longer required, backups are now achieved with
a separate command, instead of being included in CloseCollection().
- Full sync no longer requires an extra close+reopen step, and we now
wait for the backup to complete before proceeding.
- Create a backup before 'check db'
* Add File>Create Backup
https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157
* Defer checkpoint until we know we need it
When running periodic backups on a timer, we don't want to be fsync()ing
unnecessarily.
* Skip backup if modification time has not changed
We don't want the user leaving Anki open overnight, and coming back
to lots of identical backups.
* Periodic backups
Creates an automatic backup every 30 minutes if the collection has been
modified.
If there's a legacy checkpoint active, tries again 5 minutes later.
* Switch to a user-configurable backup duration
CreateBackup() now uses a simple force argument to determine whether
the user's limits should be respected or not, and only potentially
destructive ops (full download, check DB) override the user's configured
limit.
I considered having a separate limit for collection close and automatic
backups (eg keeping the previous 5 minute limit for collection close),
but that had two downsides:
- When the user closes their collection at the end of the day, they'd
get a recent backup. When they open the collection the next day, it
would get backed up again within 5 minutes, even though not much had
changed.
- Multiple limits are harder to communicate to users in the UI
Some remaining decisions I wasn't 100% sure about:
- If force is true but the collection has not been modified, the backup
will be skipped. If the user manually deleted their backups without
closing Anki, they wouldn't get a new one if the mtime hadn't changed.
- Force takes preference over the configured backup interval - should
we be ignored the user here, or take no backups at all?
Did a sneaky edit of the existing ftl string, as it hasn't been live
long.
* Move maybe_backup() into Collection
* Use a single method for manual and periodic backups
When manually creating a backup via the File menu, we no longer make
the user wait until the backup completes. As we continue waiting for
the backup in the background, if any errors occur, the user will get
notified about it fairly quickly.
* Show message to user if backup was skipped due to no changes
+ Don't incorrectly assert a backup will be created on force
* Add "automatic" to description
* Ensure we backup prior to importing colpkg if collection open
The backup doesn't happen when invoked from 'open backup' in the profile
screen, which matches Anki's previous behaviour. The user could
potentially clobber up to 30 minutes of their work if they exited to
the profile screen and restored a backup, but the alternative is we
create backups every time a backup is restored, which may happen a number
of times if the user is trying various ones. Or we could go back to a
separate throttle amount for this case, at the cost of more complexity.
* Remove the 0 special case on backup interval; minimum of 5 minutes
https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
|
|
|
# periodic backups are checked every 5 minutes
|
|
|
|
self.progress.timer(
|
|
|
|
5 * 60 * 1000,
|
|
|
|
self.on_periodic_backup_timer,
|
|
|
|
True,
|
|
|
|
parent=self,
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onRefreshTimer(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.state == "deckBrowser":
|
|
|
|
self.deckBrowser.refresh()
|
|
|
|
elif self.state == "overview":
|
|
|
|
self.overview.refresh()
|
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def on_autosync_timer(self) -> None:
|
2020-02-05 03:38:36 +01:00
|
|
|
elap = self.media_syncer.seconds_since_last_sync()
|
2020-07-01 03:35:24 +02:00
|
|
|
minutes = self.pm.auto_sync_media_minutes()
|
|
|
|
if not minutes:
|
|
|
|
return
|
|
|
|
if elap > minutes * 60:
|
2020-02-05 03:38:36 +01:00
|
|
|
self.maybe_auto_sync_media()
|
|
|
|
|
Backup improvements (#1728)
* Collection needs to be closed prior to backup even when not downgrading
* Backups -> BackupLimits
* Some improvements to backup_task
- backup_inner now returns the error instead of logging it, so that
the frontend can discover the issue when they await a backup (or create
another one)
- start_backup() was acquiring backup_task twice, and if another thread
started a backup between the two locks, the task could have been accidentally
overwritten without awaiting it
* Backups no longer require a collection close
- Instead of closing the collection, we ensure there is no active
transaction, and flush the WAL to disk. This means the undo history
is no longer lost on backup, which will be particularly useful if we
add a periodic backup in the future.
- Because a close is no longer required, backups are now achieved with
a separate command, instead of being included in CloseCollection().
- Full sync no longer requires an extra close+reopen step, and we now
wait for the backup to complete before proceeding.
- Create a backup before 'check db'
* Add File>Create Backup
https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157
* Defer checkpoint until we know we need it
When running periodic backups on a timer, we don't want to be fsync()ing
unnecessarily.
* Skip backup if modification time has not changed
We don't want the user leaving Anki open overnight, and coming back
to lots of identical backups.
* Periodic backups
Creates an automatic backup every 30 minutes if the collection has been
modified.
If there's a legacy checkpoint active, tries again 5 minutes later.
* Switch to a user-configurable backup duration
CreateBackup() now uses a simple force argument to determine whether
the user's limits should be respected or not, and only potentially
destructive ops (full download, check DB) override the user's configured
limit.
I considered having a separate limit for collection close and automatic
backups (eg keeping the previous 5 minute limit for collection close),
but that had two downsides:
- When the user closes their collection at the end of the day, they'd
get a recent backup. When they open the collection the next day, it
would get backed up again within 5 minutes, even though not much had
changed.
- Multiple limits are harder to communicate to users in the UI
Some remaining decisions I wasn't 100% sure about:
- If force is true but the collection has not been modified, the backup
will be skipped. If the user manually deleted their backups without
closing Anki, they wouldn't get a new one if the mtime hadn't changed.
- Force takes preference over the configured backup interval - should
we be ignored the user here, or take no backups at all?
Did a sneaky edit of the existing ftl string, as it hasn't been live
long.
* Move maybe_backup() into Collection
* Use a single method for manual and periodic backups
When manually creating a backup via the File menu, we no longer make
the user wait until the backup completes. As we continue waiting for
the backup in the background, if any errors occur, the user will get
notified about it fairly quickly.
* Show message to user if backup was skipped due to no changes
+ Don't incorrectly assert a backup will be created on force
* Add "automatic" to description
* Ensure we backup prior to importing colpkg if collection open
The backup doesn't happen when invoked from 'open backup' in the profile
screen, which matches Anki's previous behaviour. The user could
potentially clobber up to 30 minutes of their work if they exited to
the profile screen and restored a backup, but the alternative is we
create backups every time a backup is restored, which may happen a number
of times if the user is trying various ones. Or we could go back to a
separate throttle amount for this case, at the cost of more complexity.
* Remove the 0 special case on backup interval; minimum of 5 minutes
https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
|
|
|
# Backups
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
def on_periodic_backup_timer(self) -> None:
|
|
|
|
"""Create a backup if enough time has elapsed and collection changed."""
|
|
|
|
self._create_backup_with_progress(user_initiated=False)
|
|
|
|
|
|
|
|
def on_create_backup_now(self) -> None:
|
|
|
|
self._create_backup_with_progress(user_initiated=True)
|
|
|
|
|
|
|
|
def create_backup_now(self) -> None:
|
|
|
|
"""Create a backup immediately, regardless of when the last one was created.
|
|
|
|
Waits until the backup completes. Intended to be used as part of a longer-running
|
|
|
|
CollectionOp/QueryOp."""
|
|
|
|
self.col.create_backup(
|
|
|
|
backup_folder=self.pm.backupFolder(),
|
|
|
|
force=True,
|
|
|
|
wait_for_completion=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
def _create_backup_with_progress(self, user_initiated: bool) -> None:
|
|
|
|
# if there's a legacy undo op, try again later
|
|
|
|
if not user_initiated and self.col.legacy_checkpoint_pending():
|
|
|
|
return
|
|
|
|
|
|
|
|
# The initial copy will display a progress window if it takes too long
|
|
|
|
def backup(col: Collection) -> bool:
|
|
|
|
return col.create_backup(
|
|
|
|
backup_folder=self.pm.backupFolder(),
|
|
|
|
force=user_initiated,
|
|
|
|
wait_for_completion=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
def on_success(val: None) -> None:
|
|
|
|
if user_initiated:
|
|
|
|
tooltip(tr.profiles_backup_created(), parent=self)
|
|
|
|
|
|
|
|
def on_failure(exc: Exception) -> None:
|
|
|
|
showWarning(
|
|
|
|
tr.profiles_backup_creation_failed(reason=str(exc)), parent=self
|
|
|
|
)
|
|
|
|
|
|
|
|
def after_backup_started(created: bool) -> None:
|
|
|
|
# Legacy checkpoint may have expired.
|
|
|
|
self.update_undo_actions()
|
|
|
|
|
|
|
|
if user_initiated and not created:
|
|
|
|
tooltip(tr.profiles_backup_unchanged(), parent=self)
|
|
|
|
return
|
|
|
|
|
|
|
|
# We await backup completion to confirm it was successful, but this step
|
|
|
|
# does not block collection access, so we don't need to show the progress
|
|
|
|
# window anymore.
|
|
|
|
QueryOp(
|
|
|
|
parent=self,
|
|
|
|
op=lambda col: col.await_backup_completion(),
|
|
|
|
success=on_success,
|
|
|
|
).failure(on_failure).run_in_background()
|
|
|
|
|
|
|
|
QueryOp(parent=self, op=backup, success=after_backup_started).failure(
|
|
|
|
on_failure
|
|
|
|
).with_progress(tr.profiles_creating_backup()).run_in_background()
|
|
|
|
|
2021-03-14 13:08:37 +01:00
|
|
|
# Permanent hooks
|
2012-12-21 08:51:59 +01:00
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupHooks(self) -> None:
|
2020-01-15 07:53:24 +01:00
|
|
|
hooks.schema_will_change.append(self.onSchemaMod)
|
2020-01-15 08:45:35 +01:00
|
|
|
hooks.notes_will_be_deleted.append(self.onRemNotes)
|
2020-01-15 07:53:24 +01:00
|
|
|
hooks.card_odue_was_invalid.append(self.onOdueInvalid)
|
2018-05-31 05:20:10 +02:00
|
|
|
|
2020-01-20 13:01:38 +01:00
|
|
|
gui_hooks.av_player_will_play.append(self.on_av_player_will_play)
|
2020-01-22 05:39:18 +01:00
|
|
|
gui_hooks.av_player_did_end_playing.append(self.on_av_player_did_end_playing)
|
2021-03-14 13:08:37 +01:00
|
|
|
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
|
|
|
gui_hooks.focus_did_change.append(self.on_focus_did_change)
|
2020-01-13 05:38:05 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
self._activeWindowOnPlay: QWidget | None = None
|
2013-11-17 08:03:58 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onOdueInvalid(self) -> None:
|
2021-03-26 04:48:26 +01:00
|
|
|
showWarning(tr.qt_misc_invalid_property_found_on_card_please())
|
2013-05-22 05:27:37 +02:00
|
|
|
|
2020-01-20 13:01:38 +01:00
|
|
|
def _isVideo(self, tag: AVTag) -> bool:
|
|
|
|
if isinstance(tag, SoundOrVideoTag):
|
|
|
|
head, ext = os.path.splitext(tag.filename.lower())
|
|
|
|
return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi")
|
2018-07-23 05:19:01 +02:00
|
|
|
|
2020-01-20 13:01:38 +01:00
|
|
|
return False
|
|
|
|
|
|
|
|
def on_av_player_will_play(self, tag: AVTag) -> None:
|
|
|
|
"Record active window to restore after video playing."
|
|
|
|
if not self._isVideo(tag):
|
2018-07-23 05:19:01 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay
|
2018-05-31 05:20:10 +02:00
|
|
|
|
2020-01-22 05:39:18 +01:00
|
|
|
def on_av_player_did_end_playing(self, player: Any) -> None:
|
2020-01-20 13:01:38 +01:00
|
|
|
"Restore window focus after a video was played."
|
2018-05-31 05:20:10 +02:00
|
|
|
w = self._activeWindowOnPlay
|
2018-07-23 05:19:01 +02:00
|
|
|
if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible():
|
2018-05-31 05:20:10 +02:00
|
|
|
w.activateWindow()
|
|
|
|
w.raise_()
|
|
|
|
self._activeWindowOnPlay = None
|
2018-04-30 09:12:26 +02:00
|
|
|
|
2013-05-22 05:27:37 +02:00
|
|
|
# Log note deletion
|
|
|
|
##########################################################################
|
|
|
|
|
2021-03-27 12:38:20 +01:00
|
|
|
def onRemNotes(self, col: Collection, nids: Sequence[NoteId]) -> None:
|
2013-05-22 05:27:37 +02:00
|
|
|
path = os.path.join(self.pm.profileFolder(), "deleted.txt")
|
|
|
|
existed = os.path.exists(path)
|
2017-01-08 11:44:52 +01:00
|
|
|
with open(path, "ab") as f:
|
2013-05-22 05:27:37 +02:00
|
|
|
if not existed:
|
2017-01-08 11:44:52 +01:00
|
|
|
f.write(b"nid\tmid\tfields\n")
|
2013-05-31 03:42:24 +02:00
|
|
|
for id, mid, flds in col.db.execute(
|
2021-02-11 01:09:06 +01:00
|
|
|
f"select id, mid, flds from notes where id in {ids2str(nids)}"
|
2019-12-23 01:34:10 +01:00
|
|
|
):
|
2021-10-25 06:50:13 +02:00
|
|
|
fields = split_fields(flds)
|
2017-01-08 11:44:52 +01:00
|
|
|
f.write(("\t".join([str(id), str(mid)] + fields)).encode("utf8"))
|
|
|
|
f.write(b"\n")
|
2013-05-22 05:27:37 +02:00
|
|
|
|
|
|
|
# Schema modifications
|
|
|
|
##########################################################################
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-05-15 05:59:44 +02:00
|
|
|
# this will gradually be phased out
|
2021-02-01 11:59:18 +01:00
|
|
|
def onSchemaMod(self, arg: bool) -> bool:
|
2021-11-25 08:47:50 +01:00
|
|
|
if not self.inMainThread():
|
|
|
|
raise Exception("not in main thread")
|
2020-03-26 12:05:02 +01:00
|
|
|
progress_shown = self.progress.busy()
|
|
|
|
if progress_shown:
|
|
|
|
self.progress.finish()
|
2021-03-26 04:48:26 +01:00
|
|
|
ret = askUser(tr.qt_misc_the_requested_change_will_require_a())
|
2020-03-26 12:05:02 +01:00
|
|
|
if progress_shown:
|
|
|
|
self.progress.start()
|
|
|
|
return ret
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-05-15 05:59:44 +02:00
|
|
|
# in favour of this
|
|
|
|
def confirm_schema_modification(self) -> bool:
|
|
|
|
"""If schema unmodified, ask user to confirm change.
|
|
|
|
True if confirmed or already modified."""
|
2021-06-27 07:12:22 +02:00
|
|
|
if self.col.schema_changed():
|
2020-05-15 05:59:44 +02:00
|
|
|
return True
|
2021-03-26 04:48:26 +01:00
|
|
|
return askUser(tr.qt_misc_the_requested_change_will_require_a())
|
2020-05-15 05:59:44 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Advanced features
|
|
|
|
##########################################################################
|
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def onCheckDB(self) -> None:
|
2020-06-08 12:28:11 +02:00
|
|
|
check_db(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-10 08:58:54 +01:00
|
|
|
def on_check_media_db(self) -> None:
|
2020-11-09 10:45:14 +01:00
|
|
|
gui_hooks.media_check_will_start()
|
2020-02-10 08:58:54 +01:00
|
|
|
check_media_db(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:59:18 +01:00
|
|
|
def onStudyDeck(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.studydeck import StudyDeck
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2022-02-10 00:53:13 +01:00
|
|
|
def callback(ret: StudyDeck) -> None:
|
|
|
|
if not ret.name:
|
|
|
|
return
|
2021-04-06 13:37:31 +02:00
|
|
|
deck_id = self.col.decks.id(ret.name)
|
|
|
|
set_current_deck(parent=self, deck_id=deck_id).success(
|
|
|
|
lambda out: self.moveToState("overview")
|
|
|
|
).run_in_background()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2022-02-10 00:53:13 +01:00
|
|
|
StudyDeck(
|
|
|
|
self, dyn=True, current=self.col.decks.current()["name"], callback=callback
|
|
|
|
)
|
|
|
|
|
2020-04-25 11:44:48 +02:00
|
|
|
def onEmptyCards(self) -> None:
|
|
|
|
show_empty_cards(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Debugging
|
|
|
|
######################################################################
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onDebug(self) -> None:
|
2020-04-26 02:19:13 +02:00
|
|
|
frm = self.debug_diag_form = aqt.forms.debug.Ui_Dialog()
|
2020-05-31 01:58:00 +02:00
|
|
|
|
|
|
|
class DebugDialog(QDialog):
|
2021-03-17 05:51:59 +01:00
|
|
|
silentlyClose = True
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def reject(self) -> None:
|
2020-05-31 01:58:00 +02:00
|
|
|
super().reject()
|
|
|
|
saveSplitter(frm.splitter, "DebugConsoleWindow")
|
|
|
|
saveGeom(self, "DebugConsoleWindow")
|
|
|
|
|
|
|
|
d = self.debugDiag = DebugDialog()
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button(d)
|
2012-12-21 08:51:59 +01:00
|
|
|
frm.setupUi(d)
|
2020-05-31 01:58:00 +02:00
|
|
|
restoreGeom(d, "DebugConsoleWindow")
|
|
|
|
restoreSplitter(frm.splitter, "DebugConsoleWindow")
|
2021-10-05 05:53:01 +02:00
|
|
|
font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
|
2019-02-16 10:26:49 +01:00
|
|
|
font.setPointSize(frm.text.font().pointSize() + 1)
|
|
|
|
frm.text.setFont(font)
|
|
|
|
frm.log.setFont(font)
|
2012-12-21 08:51:59 +01:00
|
|
|
s = self.debugDiagShort = QShortcut(QKeySequence("ctrl+return"), d)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(s.activated, lambda: self.onDebugRet(frm))
|
2019-12-23 01:34:10 +01:00
|
|
|
s = self.debugDiagShort = QShortcut(QKeySequence("ctrl+shift+return"), d)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(s.activated, lambda: self.onDebugPrint(frm))
|
2019-02-16 10:31:35 +01:00
|
|
|
s = self.debugDiagShort = QShortcut(QKeySequence("ctrl+l"), d)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(s.activated, frm.log.clear)
|
2019-02-16 10:31:35 +01:00
|
|
|
s = self.debugDiagShort = QShortcut(QKeySequence("ctrl+shift+l"), d)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(s.activated, frm.text.clear)
|
2020-04-10 08:38:38 +02:00
|
|
|
|
2021-03-26 07:06:02 +01:00
|
|
|
def addContextMenu(
|
|
|
|
ev: Union[QCloseEvent, QContextMenuEvent], name: str
|
|
|
|
) -> None:
|
2020-04-10 08:38:38 +02:00
|
|
|
ev.accept()
|
|
|
|
menu = frm.log.createStandardContextMenu(QCursor.pos())
|
|
|
|
menu.addSeparator()
|
|
|
|
if name == "log":
|
2020-04-11 06:19:27 +02:00
|
|
|
a = menu.addAction("Clear Log")
|
2021-03-26 07:06:02 +01:00
|
|
|
a.setShortcut(QKeySequence("ctrl+l"))
|
2020-04-10 08:38:38 +02:00
|
|
|
qconnect(a.triggered, frm.log.clear)
|
|
|
|
elif name == "text":
|
2020-04-11 06:19:27 +02:00
|
|
|
a = menu.addAction("Clear Code")
|
2021-03-26 07:06:02 +01:00
|
|
|
a.setShortcut(QKeySequence("ctrl+shift+l"))
|
2020-04-10 08:38:38 +02:00
|
|
|
qconnect(a.triggered, frm.text.clear)
|
2021-10-05 02:01:45 +02:00
|
|
|
menu.exec(QCursor.pos())
|
2020-04-10 08:38:38 +02:00
|
|
|
|
2021-03-26 07:06:02 +01:00
|
|
|
frm.log.contextMenuEvent = lambda ev: addContextMenu(ev, "log") # type: ignore[assignment]
|
|
|
|
frm.text.contextMenuEvent = lambda ev: addContextMenu(ev, "text") # type: ignore[assignment]
|
2020-03-04 18:11:13 +01:00
|
|
|
gui_hooks.debug_console_will_show(d)
|
2012-12-21 08:51:59 +01:00
|
|
|
d.show()
|
|
|
|
|
2020-08-01 04:27:54 +02:00
|
|
|
def _captureOutput(self, on: bool) -> None:
|
2021-02-02 14:30:53 +01:00
|
|
|
mw2 = self
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-02-06 23:21:33 +01:00
|
|
|
class Stream:
|
2021-02-02 14:30:53 +01:00
|
|
|
def write(self, data: str) -> None:
|
|
|
|
mw2._output += data
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
if on:
|
|
|
|
self._output = ""
|
|
|
|
self._oldStderr = sys.stderr
|
|
|
|
self._oldStdout = sys.stdout
|
2020-08-01 04:27:54 +02:00
|
|
|
s = cast(TextIO, Stream())
|
2012-12-21 08:51:59 +01:00
|
|
|
sys.stderr = s
|
|
|
|
sys.stdout = s
|
|
|
|
else:
|
|
|
|
sys.stderr = self._oldStderr
|
|
|
|
sys.stdout = self._oldStdout
|
|
|
|
|
2020-03-23 08:44:26 +01:00
|
|
|
def _card_repr(self, card: anki.cards.Card) -> None:
|
2020-08-31 04:05:36 +02:00
|
|
|
import copy
|
|
|
|
import pprint
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-03-23 08:44:26 +01:00
|
|
|
if not card:
|
|
|
|
print("no card")
|
|
|
|
return
|
|
|
|
|
|
|
|
print("Front:", card.question())
|
|
|
|
print("\n")
|
|
|
|
print("Back:", card.answer())
|
|
|
|
|
|
|
|
print("\nNote:")
|
|
|
|
note = copy.copy(card.note())
|
|
|
|
for k, v in note.items():
|
|
|
|
print(f"- {k}:", v)
|
|
|
|
|
|
|
|
print("\n")
|
|
|
|
del note.fields
|
|
|
|
del note._fmap
|
|
|
|
pprint.pprint(note.__dict__)
|
|
|
|
|
|
|
|
print("\nCard:")
|
|
|
|
c = copy.copy(card)
|
|
|
|
c._render_output = None
|
|
|
|
pprint.pprint(c.__dict__)
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def _debugCard(self) -> anki.cards.Card | None:
|
2020-03-23 08:44:26 +01:00
|
|
|
card = self.reviewer.card
|
|
|
|
self._card_repr(card)
|
|
|
|
return card
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def _debugBrowserCard(self) -> anki.cards.Card | None:
|
2020-03-23 08:44:26 +01:00
|
|
|
card = aqt.dialogs._dialogs["Browser"][1].card
|
|
|
|
self._card_repr(card)
|
|
|
|
return card
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def onDebugPrint(self, frm: aqt.forms.debug.Ui_Dialog) -> None:
|
2019-02-16 10:57:09 +01:00
|
|
|
cursor = frm.text.textCursor()
|
|
|
|
position = cursor.position()
|
2021-10-05 05:53:01 +02:00
|
|
|
cursor.select(QTextCursor.SelectionType.LineUnderCursor)
|
2019-02-16 10:57:09 +01:00
|
|
|
line = cursor.selectedText()
|
|
|
|
pfx, sfx = "pp(", ")"
|
|
|
|
if not line.startswith(pfx):
|
2021-02-11 00:37:38 +01:00
|
|
|
line = f"{pfx}{line}{sfx}"
|
2019-02-16 10:57:09 +01:00
|
|
|
cursor.insertText(line)
|
|
|
|
cursor.setPosition(position + len(pfx))
|
|
|
|
frm.text.setTextCursor(cursor)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.onDebugRet(frm)
|
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def onDebugRet(self, frm: aqt.forms.debug.Ui_Dialog) -> None:
|
2020-08-31 04:05:36 +02:00
|
|
|
import pprint
|
|
|
|
import traceback
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
text = frm.text.toPlainText()
|
|
|
|
card = self._debugCard
|
|
|
|
bcard = self._debugBrowserCard
|
|
|
|
mw = self
|
|
|
|
pp = pprint.pprint
|
|
|
|
self._captureOutput(True)
|
|
|
|
try:
|
2019-03-04 07:01:10 +01:00
|
|
|
# pylint: disable=exec-used
|
2016-05-12 06:45:35 +02:00
|
|
|
exec(text)
|
2012-12-21 08:51:59 +01:00
|
|
|
except:
|
|
|
|
self._output += traceback.format_exc()
|
|
|
|
self._captureOutput(False)
|
|
|
|
buf = ""
|
|
|
|
for c, line in enumerate(text.strip().split("\n")):
|
|
|
|
if c == 0:
|
2021-02-11 01:09:06 +01:00
|
|
|
buf += f">>> {line}\n"
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2021-02-11 01:09:06 +01:00
|
|
|
buf += f"... {line}\n"
|
2013-04-11 08:25:59 +02:00
|
|
|
try:
|
2020-03-04 18:20:02 +01:00
|
|
|
to_append = buf + (self._output or "<no output>")
|
|
|
|
to_append = gui_hooks.debug_console_did_evaluate_python(
|
|
|
|
to_append, text, frm
|
|
|
|
)
|
|
|
|
frm.log.appendPlainText(to_append)
|
2013-04-11 08:25:59 +02:00
|
|
|
except UnicodeDecodeError:
|
2021-03-26 04:48:26 +01:00
|
|
|
to_append = tr.qt_misc_non_unicode_text()
|
2020-03-04 18:20:02 +01:00
|
|
|
to_append = gui_hooks.debug_console_did_evaluate_python(
|
|
|
|
to_append, text, frm
|
|
|
|
)
|
|
|
|
frm.log.appendPlainText(to_append)
|
2012-12-21 08:51:59 +01:00
|
|
|
frm.log.ensureCursorVisible()
|
|
|
|
|
|
|
|
# System specific code
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupSystemSpecific(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.hideMenuAccels = False
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_mac:
|
2012-12-21 08:51:59 +01:00
|
|
|
# mac users expect a minimize option
|
|
|
|
self.minimizeShortcut = QShortcut("Ctrl+M", self)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(self.minimizeShortcut.activated, self.onMacMinimize)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.hideMenuAccels = True
|
|
|
|
self.maybeHideAccelerators()
|
|
|
|
self.hideStatusTips()
|
2021-11-25 00:06:16 +01:00
|
|
|
elif is_win:
|
2020-03-18 15:36:32 +01:00
|
|
|
# make sure ctypes is bundled
|
2019-12-23 01:34:10 +01:00
|
|
|
from ctypes import windll, wintypes # type: ignore
|
|
|
|
|
2020-03-17 04:24:37 +01:00
|
|
|
_dummy1 = windll
|
|
|
|
_dummy2 = wintypes
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def maybeHideAccelerators(self, tgt: Any | None = None) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.hideMenuAccels:
|
|
|
|
return
|
|
|
|
tgt = tgt or self
|
2021-03-17 05:51:59 +01:00
|
|
|
for action_ in tgt.findChildren(QAction):
|
|
|
|
action = cast(QAction, action_)
|
2016-05-12 06:45:35 +02:00
|
|
|
txt = str(action.text())
|
2017-12-11 08:25:51 +01:00
|
|
|
m = re.match(r"^(.+)\(&.+\)(.+)?", txt)
|
2012-12-21 08:51:59 +01:00
|
|
|
if m:
|
|
|
|
action.setText(m.group(1) + (m.group(2) or ""))
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def hideStatusTips(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
for action in self.findChildren(QAction):
|
2021-03-17 05:51:59 +01:00
|
|
|
cast(QAction, action).setStatusTip("")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-08-01 04:27:54 +02:00
|
|
|
def onMacMinimize(self) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
self.setWindowState(self.windowState() | Qt.WindowState.WindowMinimized) # type: ignore
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Single instance support
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupAppMsg(self) -> None:
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(self.app.appMsg, self.onAppMsg)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def onAppMsg(self, buf: str) -> None:
|
2020-01-04 04:30:33 +01:00
|
|
|
is_addon = self._isAddon(buf)
|
2020-01-03 18:23:28 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.state == "startup":
|
|
|
|
# try again in a second
|
2022-02-24 12:15:56 +01:00
|
|
|
self.progress.single_shot(
|
2022-02-18 10:00:12 +01:00
|
|
|
1000,
|
|
|
|
lambda: self.onAppMsg(buf),
|
|
|
|
False,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2021-02-01 11:23:48 +01:00
|
|
|
return
|
2012-12-21 08:51:59 +01:00
|
|
|
elif self.state == "profileManager":
|
|
|
|
# can't raise window while in profile manager
|
|
|
|
if buf == "raise":
|
2019-12-20 09:43:52 +01:00
|
|
|
return None
|
2012-12-21 08:51:59 +01:00
|
|
|
self.pendingImport = buf
|
2020-01-03 17:57:33 +01:00
|
|
|
if is_addon:
|
2021-03-26 04:48:26 +01:00
|
|
|
msg = tr.qt_misc_addon_will_be_installed_when_a()
|
2020-01-03 17:57:33 +01:00
|
|
|
else:
|
2021-03-26 04:48:26 +01:00
|
|
|
msg = tr.qt_misc_deck_will_be_imported_when_a()
|
2021-02-01 11:23:48 +01:00
|
|
|
tooltip(msg)
|
|
|
|
return
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.interactiveState() or self.progress.busy():
|
|
|
|
# we can't raise the main window while in profile dialog, syncing, etc
|
|
|
|
if buf != "raise":
|
2019-12-23 01:34:10 +01:00
|
|
|
showInfo(
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.qt_misc_please_ensure_a_profile_is_open(),
|
2019-12-23 01:34:10 +01:00
|
|
|
parent=None,
|
|
|
|
)
|
2019-12-20 09:43:52 +01:00
|
|
|
return None
|
2012-12-21 08:51:59 +01:00
|
|
|
# raise window
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_win:
|
2012-12-21 08:51:59 +01:00
|
|
|
# on windows we can raise the window by minimizing and restoring
|
|
|
|
self.showMinimized()
|
2021-10-05 05:53:01 +02:00
|
|
|
self.setWindowState(Qt.WindowState.WindowActive)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.showNormal()
|
|
|
|
else:
|
|
|
|
# on osx we can raise the window. on unity the icon in the tray will just flash.
|
|
|
|
self.activateWindow()
|
|
|
|
self.raise_()
|
|
|
|
if buf == "raise":
|
2019-12-20 09:43:52 +01:00
|
|
|
return None
|
2020-01-03 18:23:28 +01:00
|
|
|
|
2020-01-03 17:57:33 +01:00
|
|
|
# import / add-on installation
|
|
|
|
if is_addon:
|
|
|
|
self.installAddon(buf)
|
|
|
|
else:
|
|
|
|
self.handleImport(buf)
|
2020-01-03 18:23:28 +01:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
return None
|
2016-07-04 05:22:35 +02:00
|
|
|
|
2020-01-04 04:30:33 +01:00
|
|
|
def _isAddon(self, buf: str) -> bool:
|
|
|
|
return buf.endswith(self.addonManager.ext)
|
|
|
|
|
2021-03-14 13:08:37 +01:00
|
|
|
def interactiveState(self) -> bool:
|
|
|
|
"True if not in profile manager, syncing, etc."
|
|
|
|
return self.state in ("overview", "review", "deckBrowser")
|
|
|
|
|
2016-07-04 05:22:35 +02:00
|
|
|
# GC
|
|
|
|
##########################################################################
|
2021-03-05 04:07:52 +01:00
|
|
|
# The default Python garbage collection can trigger on any thread. This can
|
|
|
|
# cause crashes if Qt objects are garbage-collected, as Qt expects access
|
|
|
|
# only on the main thread. So Anki disables the default GC on startup, and
|
|
|
|
# instead runs it on a timer, and after dialog close.
|
|
|
|
# The gc after dialog close is necessary to free up the memory and extra
|
|
|
|
# processes that webviews spawn, as a lot of the GUI code creates ref cycles.
|
|
|
|
|
|
|
|
def garbage_collect_on_dialog_finish(self, dialog: QDialog) -> None:
|
|
|
|
qconnect(
|
|
|
|
dialog.finished, lambda: self.deferred_delete_and_garbage_collect(dialog)
|
|
|
|
)
|
2016-07-04 05:22:35 +02:00
|
|
|
|
2021-03-05 04:07:52 +01:00
|
|
|
def deferred_delete_and_garbage_collect(self, obj: QObject) -> None:
|
2016-07-04 05:22:35 +02:00
|
|
|
obj.deleteLater()
|
2022-02-24 12:15:56 +01:00
|
|
|
self.progress.single_shot(1000, self.garbage_collect_now, False)
|
2016-07-04 05:22:35 +02:00
|
|
|
|
2021-03-05 04:07:52 +01:00
|
|
|
def disable_automatic_garbage_collection(self) -> None:
|
2017-01-08 05:42:50 +01:00
|
|
|
gc.collect()
|
|
|
|
gc.disable()
|
|
|
|
|
2021-03-05 04:07:52 +01:00
|
|
|
def garbage_collect_now(self) -> None:
|
|
|
|
# gc.collect() has optional arguments that will cause problems if
|
|
|
|
# it's passed directly to a QTimer, and pylint complains if we
|
|
|
|
# wrap it in a lambda, so we use this trivial wrapper
|
2016-07-04 05:22:35 +02:00
|
|
|
gc.collect()
|
2016-07-07 15:39:48 +02:00
|
|
|
|
2021-03-05 04:07:52 +01:00
|
|
|
# legacy aliases
|
|
|
|
|
|
|
|
setupDialogGC = garbage_collect_on_dialog_finish
|
|
|
|
gcWindow = deferred_delete_and_garbage_collect
|
|
|
|
|
2016-07-07 15:39:48 +02:00
|
|
|
# Media server
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def setupMediaServer(self) -> None:
|
2019-03-02 18:57:51 +01:00
|
|
|
self.mediaServer = aqt.mediasrv.MediaServer(self)
|
2016-07-07 15:39:48 +02:00
|
|
|
self.mediaServer.start()
|
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def baseHTML(self) -> str:
|
2021-02-11 01:09:06 +01:00
|
|
|
return f'<base href="{self.serverURL()}">'
|
2017-08-11 12:37:04 +02:00
|
|
|
|
2019-12-20 09:43:52 +01:00
|
|
|
def serverURL(self) -> str:
|
2018-11-12 13:23:47 +01:00
|
|
|
return "http://127.0.0.1:%d/" % self.mediaServer.getPort()
|
2021-03-14 13:08:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
# legacy
|
|
|
|
class ResetReason(enum.Enum):
|
|
|
|
Unknown = "unknown"
|
|
|
|
AddCardsAddNote = "addCardsAddNote"
|
|
|
|
EditCurrentInit = "editCurrentInit"
|
|
|
|
EditorBridgeCmd = "editorBridgeCmd"
|
|
|
|
BrowserSetDeck = "browserSetDeck"
|
|
|
|
BrowserAddTags = "browserAddTags"
|
|
|
|
BrowserRemoveTags = "browserRemoveTags"
|
|
|
|
BrowserSuspend = "browserSuspend"
|
|
|
|
BrowserReposition = "browserReposition"
|
|
|
|
BrowserReschedule = "browserReschedule"
|
|
|
|
BrowserFindReplace = "browserFindReplace"
|
|
|
|
BrowserTagDupes = "browserTagDupes"
|
|
|
|
BrowserDeleteDeck = "browserDeleteDeck"
|