mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Merge remote-tracking branch 'danielelmes/master' into fix_windows_build
# Conflicts: # Makefile
This commit is contained in:
commit
dc049ce26a
46 changed files with 371 additions and 236 deletions
2
.github/scripts/contrib.sh
vendored
2
.github/scripts/contrib.sh
vendored
|
@ -9,7 +9,7 @@ git log --pretty=format:' - %ae' CONTRIBUTORS |sort |uniq |sort -f | sed "s/@/$a
|
||||||
|
|
||||||
headAuthor=$(git log -1 --pretty=format:'%ae')
|
headAuthor=$(git log -1 --pretty=format:'%ae')
|
||||||
authorAt=$(echo "$headAuthor" | sed "s/@/$antispam/")
|
authorAt=$(echo "$headAuthor" | sed "s/@/$antispam/")
|
||||||
if git log --pretty=format:'%ae' CONTRIBUTORS | grep -qi "$headAuthor"; then
|
if git log --pretty=format:'%ae' CONTRIBUTORS | grep -i "$headAuthor" > /dev/null; then
|
||||||
echo "Author $authorAt found in CONTRIBUTORS"
|
echo "Author $authorAt found in CONTRIBUTORS"
|
||||||
else
|
else
|
||||||
echo "Author $authorAt NOT found in list"
|
echo "Author $authorAt NOT found in list"
|
||||||
|
|
16
Makefile
16
Makefile
|
@ -44,7 +44,7 @@ buildhash:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
.PHONY: develop
|
.PHONY: develop
|
||||||
develop: pyenv buildhash
|
develop: pyenv buildhash prepare
|
||||||
set -eo pipefail && \
|
set -eo pipefail && \
|
||||||
. "${ACTIVATE_SCRIPT}" && \
|
. "${ACTIVATE_SCRIPT}" && \
|
||||||
for dir in $(DEVEL); do \
|
for dir in $(DEVEL); do \
|
||||||
|
@ -58,6 +58,16 @@ run: develop
|
||||||
echo "Starting Anki..."; \
|
echo "Starting Anki..."; \
|
||||||
python qt/runanki $(RUNFLAGS)
|
python qt/runanki $(RUNFLAGS)
|
||||||
|
|
||||||
|
.PHONY: prepare
|
||||||
|
prepare: rslib/ftl/repo qt/ftl/repo qt/po/repo
|
||||||
|
|
||||||
|
rslib/ftl/repo:
|
||||||
|
$(MAKE) pull-i18n
|
||||||
|
qt/ftl/repo:
|
||||||
|
$(MAKE) pull-i18n
|
||||||
|
qt/po/repo:
|
||||||
|
$(MAKE) pull-i18n
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: clean-dist build-rspy build-pylib build-qt add-buildhash
|
build: clean-dist build-rspy build-pylib build-qt add-buildhash
|
||||||
@echo
|
@echo
|
||||||
|
@ -123,10 +133,10 @@ add-buildhash:
|
||||||
pull-i18n:
|
pull-i18n:
|
||||||
(cd rslib/ftl && scripts/fetch-latest-translations)
|
(cd rslib/ftl && scripts/fetch-latest-translations)
|
||||||
(cd qt/ftl && scripts/fetch-latest-translations)
|
(cd qt/ftl && scripts/fetch-latest-translations)
|
||||||
(cd qt/i18n && ./pull-git)
|
(cd qt/po && scripts/fetch-latest-translations)
|
||||||
|
|
||||||
.PHONY: push-i18n
|
.PHONY: push-i18n
|
||||||
push-i18n: pull-i18n
|
push-i18n: pull-i18n
|
||||||
(cd rslib/ftl && scripts/upload-latest-templates)
|
(cd rslib/ftl && scripts/upload-latest-templates)
|
||||||
(cd qt/ftl && scripts/upload-latest-templates)
|
(cd qt/ftl && scripts/upload-latest-templates)
|
||||||
(cd qt/i18n && ./sync-po-git)
|
(cd qt/po && scripts/upload-latest-template)
|
||||||
|
|
|
@ -96,6 +96,16 @@ pylib/tools/genhooks.py and qt/tools/genhooks_gui.py. Adding a new definition
|
||||||
in one of those files and running 'make develop' will update pylib/anki/hooks
|
in one of those files and running 'make develop' will update pylib/anki/hooks
|
||||||
.py or qt/aqt/gui_hooks.py.
|
.py or qt/aqt/gui_hooks.py.
|
||||||
|
|
||||||
|
Translations
|
||||||
|
--------------
|
||||||
|
|
||||||
|
The translations into other languages will be fetched on the first build.
|
||||||
|
If you'd like to keep them up to date, you need to run 'make pull-i18n'
|
||||||
|
periodically.
|
||||||
|
|
||||||
|
For information on adding new translatable strings to Anki, please see
|
||||||
|
https://ankitects.github.io/translating/#/anki/developers
|
||||||
|
|
||||||
Tests Must Pass
|
Tests Must Pass
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,9 @@ message I18nBackendInit {
|
||||||
string locale_folder_path = 5;
|
string locale_folder_path = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1-15 reserved for future use; 2047 for errors
|
// 1-15 reserved for future use
|
||||||
|
|
||||||
message BackendInput {
|
message BackendInput {
|
||||||
reserved 2047;
|
|
||||||
oneof value {
|
oneof value {
|
||||||
TemplateRequirementsIn template_requirements = 16;
|
TemplateRequirementsIn template_requirements = 16;
|
||||||
SchedTimingTodayIn sched_timing_today = 17;
|
SchedTimingTodayIn sched_timing_today = 17;
|
||||||
|
@ -47,37 +46,43 @@ message BackendInput {
|
||||||
|
|
||||||
message BackendOutput {
|
message BackendOutput {
|
||||||
oneof value {
|
oneof value {
|
||||||
TemplateRequirementsOut template_requirements = 16;
|
// infallible commands
|
||||||
SchedTimingTodayOut sched_timing_today = 17;
|
SchedTimingTodayOut sched_timing_today = 17;
|
||||||
DeckTreeOut deck_tree = 18;
|
|
||||||
FindCardsOut find_cards = 19;
|
|
||||||
BrowserRowsOut browser_rows = 20;
|
|
||||||
RenderCardOut render_card = 21;
|
|
||||||
sint32 local_minutes_west = 22;
|
sint32 local_minutes_west = 22;
|
||||||
string strip_av_tags = 23;
|
string strip_av_tags = 23;
|
||||||
ExtractAVTagsOut extract_av_tags = 24;
|
ExtractAVTagsOut extract_av_tags = 24;
|
||||||
ExtractLatexOut extract_latex = 25;
|
ExtractLatexOut extract_latex = 25;
|
||||||
string add_media_file = 26;
|
|
||||||
Empty sync_media = 27;
|
|
||||||
MediaCheckOut check_media = 28;
|
|
||||||
Empty trash_media_files = 29;
|
|
||||||
string translate_string = 30;
|
string translate_string = 30;
|
||||||
string format_time_span = 31;
|
string format_time_span = 31;
|
||||||
string studied_today = 32;
|
string studied_today = 32;
|
||||||
string congrats_learn_msg = 33;
|
string congrats_learn_msg = 33;
|
||||||
|
|
||||||
|
// fallible commands
|
||||||
|
TemplateRequirementsOut template_requirements = 16;
|
||||||
|
DeckTreeOut deck_tree = 18;
|
||||||
|
FindCardsOut find_cards = 19;
|
||||||
|
BrowserRowsOut browser_rows = 20;
|
||||||
|
RenderCardOut render_card = 21;
|
||||||
|
string add_media_file = 26;
|
||||||
|
Empty sync_media = 27;
|
||||||
|
MediaCheckOut check_media = 28;
|
||||||
|
Empty trash_media_files = 29;
|
||||||
|
|
||||||
BackendError error = 2047;
|
BackendError error = 2047;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message BackendError {
|
message BackendError {
|
||||||
|
// localized error description suitable for displaying to the user
|
||||||
|
string localized = 1;
|
||||||
|
// error specifics
|
||||||
oneof value {
|
oneof value {
|
||||||
StringError invalid_input = 1;
|
Empty invalid_input = 2;
|
||||||
TemplateParseError template_parse = 2;
|
Empty template_parse = 3;
|
||||||
StringError io_error = 3;
|
Empty io_error = 4;
|
||||||
StringError db_error = 4;
|
Empty db_error = 5;
|
||||||
NetworkError network_error = 5;
|
NetworkError network_error = 6;
|
||||||
SyncError sync_error = 6;
|
SyncError sync_error = 7;
|
||||||
// user interrupted operation
|
// user interrupted operation
|
||||||
Empty interrupted = 8;
|
Empty interrupted = 8;
|
||||||
}
|
}
|
||||||
|
@ -90,28 +95,17 @@ message Progress {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message StringError {
|
|
||||||
string info = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message TemplateParseError {
|
|
||||||
string info = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message NetworkError {
|
message NetworkError {
|
||||||
string info = 1;
|
|
||||||
enum NetworkErrorKind {
|
enum NetworkErrorKind {
|
||||||
OTHER = 0;
|
OTHER = 0;
|
||||||
OFFLINE = 1;
|
OFFLINE = 1;
|
||||||
TIMEOUT = 2;
|
TIMEOUT = 2;
|
||||||
PROXY_AUTH = 3;
|
PROXY_AUTH = 3;
|
||||||
}
|
}
|
||||||
NetworkErrorKind kind = 2;
|
NetworkErrorKind kind = 1;
|
||||||
string localized = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SyncError {
|
message SyncError {
|
||||||
string info = 1;
|
|
||||||
enum SyncErrorKind {
|
enum SyncErrorKind {
|
||||||
OTHER = 0;
|
OTHER = 0;
|
||||||
CONFLICT = 1;
|
CONFLICT = 1;
|
||||||
|
@ -122,8 +116,7 @@ message SyncError {
|
||||||
MEDIA_CHECK_REQUIRED = 6;
|
MEDIA_CHECK_REQUIRED = 6;
|
||||||
RESYNC_REQUIRED = 7;
|
RESYNC_REQUIRED = 7;
|
||||||
}
|
}
|
||||||
SyncErrorKind kind = 2;
|
SyncErrorKind kind = 1;
|
||||||
string localized = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message MediaSyncProgress {
|
message MediaSyncProgress {
|
||||||
|
@ -307,8 +300,9 @@ message TranslateArgValue {
|
||||||
|
|
||||||
message FormatTimeSpanIn {
|
message FormatTimeSpanIn {
|
||||||
enum Context {
|
enum Context {
|
||||||
NORMAL = 0;
|
PRECISE = 0;
|
||||||
ANSWER_BUTTONS = 1;
|
ANSWER_BUTTONS = 1;
|
||||||
|
INTERVALS = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
float seconds = 1;
|
float seconds = 1;
|
||||||
|
|
|
@ -73,7 +73,6 @@ class _Collection:
|
||||||
ls: int
|
ls: int
|
||||||
conf: Dict[str, Any]
|
conf: Dict[str, Any]
|
||||||
_undo: List[Any]
|
_undo: List[Any]
|
||||||
backend: RustBackend
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -83,6 +82,7 @@ class _Collection:
|
||||||
log: bool = False,
|
log: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
self.tr = backend.translate
|
||||||
self._debugLog = log
|
self._debugLog = log
|
||||||
self.db = db
|
self.db = db
|
||||||
self.path = db._path
|
self.path = db._path
|
||||||
|
|
|
@ -12,7 +12,7 @@ import ankirspy # pytype: disable=import-error
|
||||||
import anki.backend_pb2 as pb
|
import anki.backend_pb2 as pb
|
||||||
import anki.buildinfo
|
import anki.buildinfo
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.fluent_pb2 import FluentString as FString
|
from anki.fluent_pb2 import FluentString as TR
|
||||||
from anki.models import AllTemplateReqs
|
from anki.models import AllTemplateReqs
|
||||||
from anki.sound import AVTag, SoundOrVideoTag, TTSTag
|
from anki.sound import AVTag, SoundOrVideoTag, TTSTag
|
||||||
from anki.types import assert_impossible_literal
|
from anki.types import assert_impossible_literal
|
||||||
|
@ -32,14 +32,17 @@ class StringError(Exception):
|
||||||
|
|
||||||
|
|
||||||
NetworkErrorKind = pb.NetworkError.NetworkErrorKind
|
NetworkErrorKind = pb.NetworkError.NetworkErrorKind
|
||||||
|
SyncErrorKind = pb.SyncError.SyncErrorKind
|
||||||
|
|
||||||
|
|
||||||
class NetworkError(StringError):
|
class NetworkError(StringError):
|
||||||
def kind(self) -> NetworkErrorKind:
|
def kind(self) -> NetworkErrorKind:
|
||||||
return self.args[1]
|
return self.args[1]
|
||||||
|
|
||||||
def localized(self) -> str:
|
|
||||||
return self.args[2]
|
class SyncError(StringError):
|
||||||
|
def kind(self) -> SyncErrorKind:
|
||||||
|
return self.args[1]
|
||||||
|
|
||||||
|
|
||||||
class IOError(StringError):
|
class IOError(StringError):
|
||||||
|
@ -54,35 +57,22 @@ class TemplateError(StringError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
SyncErrorKind = pb.SyncError.SyncErrorKind
|
|
||||||
|
|
||||||
|
|
||||||
class SyncError(StringError):
|
|
||||||
def kind(self) -> SyncErrorKind:
|
|
||||||
return self.args[1]
|
|
||||||
|
|
||||||
def localized(self) -> str:
|
|
||||||
return self.args[2]
|
|
||||||
|
|
||||||
|
|
||||||
def proto_exception_to_native(err: pb.BackendError) -> Exception:
|
def proto_exception_to_native(err: pb.BackendError) -> Exception:
|
||||||
val = err.WhichOneof("value")
|
val = err.WhichOneof("value")
|
||||||
if val == "interrupted":
|
if val == "interrupted":
|
||||||
return Interrupted()
|
return Interrupted()
|
||||||
elif val == "network_error":
|
elif val == "network_error":
|
||||||
e = err.network_error
|
return NetworkError(err.localized, err.network_error.kind)
|
||||||
return NetworkError(e.info, e.kind, e.localized)
|
|
||||||
elif val == "io_error":
|
|
||||||
return IOError(err.io_error.info)
|
|
||||||
elif val == "db_error":
|
|
||||||
return DBError(err.db_error.info)
|
|
||||||
elif val == "template_parse":
|
|
||||||
return TemplateError(err.template_parse.info)
|
|
||||||
elif val == "invalid_input":
|
|
||||||
return StringError(err.invalid_input.info)
|
|
||||||
elif val == "sync_error":
|
elif val == "sync_error":
|
||||||
e2 = err.sync_error
|
return SyncError(err.localized, err.sync_error.kind)
|
||||||
return SyncError(e2.info, e2.kind, e2.localized)
|
elif val == "io_error":
|
||||||
|
return IOError(err.localized)
|
||||||
|
elif val == "db_error":
|
||||||
|
return DBError(err.localized)
|
||||||
|
elif val == "template_parse":
|
||||||
|
return TemplateError(err.localized)
|
||||||
|
elif val == "invalid_input":
|
||||||
|
return StringError(err.localized)
|
||||||
else:
|
else:
|
||||||
assert_impossible_literal(val)
|
assert_impossible_literal(val)
|
||||||
|
|
||||||
|
@ -334,7 +324,7 @@ class RustBackend:
|
||||||
pb.BackendInput(trash_media_files=pb.TrashMediaFilesIn(fnames=fnames))
|
pb.BackendInput(trash_media_files=pb.TrashMediaFilesIn(fnames=fnames))
|
||||||
)
|
)
|
||||||
|
|
||||||
def translate(self, key: FString, **kwargs: Union[str, int, float]):
|
def translate(self, key: TR, **kwargs: Union[str, int, float]) -> str:
|
||||||
return self._run_command(
|
return self._run_command(
|
||||||
pb.BackendInput(translate_string=translate_string_in(key, **kwargs))
|
pb.BackendInput(translate_string=translate_string_in(key, **kwargs))
|
||||||
).translate_string
|
).translate_string
|
||||||
|
@ -342,7 +332,7 @@ class RustBackend:
|
||||||
def format_time_span(
|
def format_time_span(
|
||||||
self,
|
self,
|
||||||
seconds: float,
|
seconds: float,
|
||||||
context: FormatTimeSpanContext = FormatTimeSpanContext.NORMAL,
|
context: FormatTimeSpanContext = FormatTimeSpanContext.INTERVALS,
|
||||||
) -> str:
|
) -> str:
|
||||||
return self._run_command(
|
return self._run_command(
|
||||||
pb.BackendInput(
|
pb.BackendInput(
|
||||||
|
@ -368,7 +358,7 @@ class RustBackend:
|
||||||
|
|
||||||
|
|
||||||
def translate_string_in(
|
def translate_string_in(
|
||||||
key: FString, **kwargs: Union[str, int, float]
|
key: TR, **kwargs: Union[str, int, float]
|
||||||
) -> pb.TranslateStringIn:
|
) -> pb.TranslateStringIn:
|
||||||
args = {}
|
args = {}
|
||||||
for (k, v) in kwargs.items():
|
for (k, v) in kwargs.items():
|
||||||
|
@ -386,7 +376,7 @@ class I18nBackend:
|
||||||
)
|
)
|
||||||
self._backend = ankirspy.open_i18n(init_msg.SerializeToString())
|
self._backend = ankirspy.open_i18n(init_msg.SerializeToString())
|
||||||
|
|
||||||
def translate(self, key: FString, **kwargs: Union[str, int, float]):
|
def translate(self, key: TR, **kwargs: Union[str, int, float]):
|
||||||
return self._backend.translate(
|
return self._backend.translate(
|
||||||
translate_string_in(key, **kwargs).SerializeToString()
|
translate_string_in(key, **kwargs).SerializeToString()
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||||
import anki
|
import anki
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
from anki.rsbackend import FString
|
from anki.rsbackend import TR, FormatTimeSpanContext
|
||||||
from anki.utils import ids2str
|
from anki.utils import ids2str
|
||||||
|
|
||||||
# Card stats
|
# Card stats
|
||||||
|
@ -47,9 +47,7 @@ class CardStats:
|
||||||
next = c.due
|
next = c.due
|
||||||
next = self.date(next)
|
next = self.date(next)
|
||||||
if next:
|
if next:
|
||||||
self.addLine(
|
self.addLine(self.col.tr(TR.STATISTICS_DUE_DATE), next)
|
||||||
self.col.backend.translate(FString.STATISTICS_DUE_DATE), next,
|
|
||||||
)
|
|
||||||
if c.queue == QUEUE_TYPE_REV:
|
if c.queue == QUEUE_TYPE_REV:
|
||||||
self.addLine(
|
self.addLine(
|
||||||
_("Interval"), self.col.backend.format_time_span(c.ivl * 86400)
|
_("Interval"), self.col.backend.format_time_span(c.ivl * 86400)
|
||||||
|
@ -85,7 +83,9 @@ class CardStats:
|
||||||
return time.strftime("%Y-%m-%d", time.localtime(tm))
|
return time.strftime("%Y-%m-%d", time.localtime(tm))
|
||||||
|
|
||||||
def time(self, tm: float) -> str:
|
def time(self, tm: float) -> str:
|
||||||
return self.col.backend.format_time_span(tm)
|
return self.col.backend.format_time_span(
|
||||||
|
tm, context=FormatTimeSpanContext.PRECISE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Collection stats
|
# Collection stats
|
||||||
|
@ -276,9 +276,7 @@ from revlog where id > ? """
|
||||||
def _dueInfo(self, tot, num) -> str:
|
def _dueInfo(self, tot, num) -> str:
|
||||||
i: List[str] = []
|
i: List[str] = []
|
||||||
self._line(
|
self._line(
|
||||||
i,
|
i, _("Total"), self.col.tr(TR.STATISTICS_REVIEWS, reviews=tot),
|
||||||
_("Total"),
|
|
||||||
self.col.backend.translate(FString.STATISTICS_REVIEWS, reviews=tot),
|
|
||||||
)
|
)
|
||||||
self._line(i, _("Average"), self._avgDay(tot, num, _("reviews")))
|
self._line(i, _("Average"), self._avgDay(tot, num, _("reviews")))
|
||||||
tomorrow = self.col.db.scalar(
|
tomorrow = self.col.db.scalar(
|
||||||
|
@ -455,8 +453,8 @@ group by day order by day"""
|
||||||
self._line(
|
self._line(
|
||||||
i,
|
i,
|
||||||
_("Average answer time"),
|
_("Average answer time"),
|
||||||
self.col.backend.translate(
|
self.col.tr(
|
||||||
FString.STATISTICS_AVERAGE_ANSWER_TIME,
|
TR.STATISTICS_AVERAGE_ANSWER_TIME,
|
||||||
**{"cards-per-minute": perMin, "average-seconds": average_secs},
|
**{"cards-per-minute": perMin, "average-seconds": average_secs},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import tempfile
|
||||||
|
|
||||||
from anki import Collection as aopen
|
from anki import Collection as aopen
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from anki.rsbackend import FString
|
from anki.rsbackend import TR
|
||||||
from anki.stdmodels import addBasicModel, models
|
from anki.stdmodels import addBasicModel, models
|
||||||
from anki.utils import isWin
|
from anki.utils import isWin
|
||||||
from tests.shared import assertException, getEmptyCol
|
from tests.shared import assertException, getEmptyCol
|
||||||
|
@ -153,12 +153,11 @@ def test_furigana():
|
||||||
|
|
||||||
def test_translate():
|
def test_translate():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
tr = d.backend.translate
|
|
||||||
no_uni = without_unicode_isolation
|
no_uni = without_unicode_isolation
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
tr(FString.CARD_TEMPLATE_RENDERING_FRONT_SIDE_PROBLEM)
|
d.tr(TR.CARD_TEMPLATE_RENDERING_FRONT_SIDE_PROBLEM)
|
||||||
== "Front template has a problem:"
|
== "Front template has a problem:"
|
||||||
)
|
)
|
||||||
assert no_uni(tr(FString.STATISTICS_REVIEWS, reviews=1)) == "1 review"
|
assert no_uni(d.tr(TR.STATISTICS_REVIEWS, reviews=1)) == "1 review"
|
||||||
assert no_uni(tr(FString.STATISTICS_REVIEWS, reviews=2)) == "2 reviews"
|
assert no_uni(d.tr(TR.STATISTICS_REVIEWS, reviews=2)) == "2 reviews"
|
||||||
|
|
|
@ -27,8 +27,10 @@ all: check
|
||||||
./tools/build_ui.sh
|
./tools/build_ui.sh
|
||||||
@touch $@
|
@touch $@
|
||||||
|
|
||||||
.build/i18n: $(wildcard i18n/po/desktop/*/anki.po)
|
.build/i18n: $(wildcard po/repo/desktop/*/anki.po)
|
||||||
(cd i18n && ./pull-git && ./build-mo-files && ./copy-qt-files)
|
(cd po && ./scripts/fetch-latest-translations && \
|
||||||
|
./scripts/build-mo-files && \
|
||||||
|
./scripts/copy-qt-files)
|
||||||
@touch $@
|
@touch $@
|
||||||
|
|
||||||
TSDEPS := $(wildcard ts/src/*.ts) $(wildcard ts/scss/*.scss)
|
TSDEPS := $(wildcard ts/src/*.ts) $(wildcard ts/scss/*.scss)
|
||||||
|
|
|
@ -24,7 +24,7 @@ from anki.consts import *
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
from anki.models import NoteType
|
from anki.models import NoteType
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
from anki.rsbackend import FString
|
from anki.rsbackend import TR
|
||||||
from anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin
|
from anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.editor import Editor
|
from aqt.editor import Editor
|
||||||
|
@ -356,7 +356,7 @@ class DataModel(QAbstractTableModel):
|
||||||
elif c.queue == QUEUE_TYPE_LRN:
|
elif c.queue == QUEUE_TYPE_LRN:
|
||||||
date = c.due
|
date = c.due
|
||||||
elif c.queue == QUEUE_TYPE_NEW or c.type == CARD_TYPE_NEW:
|
elif c.queue == QUEUE_TYPE_NEW or c.type == CARD_TYPE_NEW:
|
||||||
return tr(FString.STATISTICS_DUE_FOR_NEW_CARD, number=c.due)
|
return tr(TR.STATISTICS_DUE_FOR_NEW_CARD, number=c.due)
|
||||||
elif c.queue in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN) or (
|
elif c.queue in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN) or (
|
||||||
c.type == CARD_TYPE_REV and c.queue < 0
|
c.type == CARD_TYPE_REV and c.queue < 0
|
||||||
):
|
):
|
||||||
|
@ -730,7 +730,7 @@ class Browser(QMainWindow):
|
||||||
("noteCrt", _("Created")),
|
("noteCrt", _("Created")),
|
||||||
("noteMod", _("Edited")),
|
("noteMod", _("Edited")),
|
||||||
("cardMod", _("Changed")),
|
("cardMod", _("Changed")),
|
||||||
("cardDue", tr(FString.STATISTICS_DUE_DATE)),
|
("cardDue", tr(TR.STATISTICS_DUE_DATE)),
|
||||||
("cardIvl", _("Interval")),
|
("cardIvl", _("Interval")),
|
||||||
("cardEase", _("Ease")),
|
("cardEase", _("Ease")),
|
||||||
("cardReps", _("Reviews")),
|
("cardReps", _("Reviews")),
|
||||||
|
@ -1281,7 +1281,7 @@ by clicking on one on the left."""
|
||||||
(_("New"), "is:new"),
|
(_("New"), "is:new"),
|
||||||
(_("Learning"), "is:learn"),
|
(_("Learning"), "is:learn"),
|
||||||
(_("Review"), "is:review"),
|
(_("Review"), "is:review"),
|
||||||
(tr(FString.FILTERING_IS_DUE), "is:due"),
|
(tr(TR.FILTERING_IS_DUE), "is:due"),
|
||||||
None,
|
None,
|
||||||
(_("Suspended"), "is:suspended"),
|
(_("Suspended"), "is:suspended"),
|
||||||
(_("Buried"), "is:buried"),
|
(_("Buried"), "is:buried"),
|
||||||
|
@ -1494,6 +1494,8 @@ border: 1px solid #000; padding: 3px; '>%s</div>"""
|
||||||
if ivl == 0:
|
if ivl == 0:
|
||||||
ivl = ""
|
ivl = ""
|
||||||
else:
|
else:
|
||||||
|
if ivl > 0:
|
||||||
|
ivl *= 86_400
|
||||||
ivl = cs.time(abs(ivl))
|
ivl = cs.time(abs(ivl))
|
||||||
s += "<td align=right>%s</td>" % tstr
|
s += "<td align=right>%s</td>" % tstr
|
||||||
s += "<td align=center>%s</td>" % ease
|
s += "<td align=center>%s</td>" % ease
|
||||||
|
@ -1501,7 +1503,7 @@ border: 1px solid #000; padding: 3px; '>%s</div>"""
|
||||||
|
|
||||||
s += ("<td align=right>%s</td>" * 2) % (
|
s += ("<td align=right>%s</td>" * 2) % (
|
||||||
"%d%%" % (factor / 10) if factor else "",
|
"%d%%" % (factor / 10) if factor else "",
|
||||||
cs.time(taken),
|
self.col.backend.format_time_span(taken),
|
||||||
) + "</tr>"
|
) + "</tr>"
|
||||||
s += "</table>"
|
s += "</table>"
|
||||||
if cnt < self.card.reps:
|
if cnt < self.card.reps:
|
||||||
|
@ -1770,6 +1772,8 @@ where id in %s"""
|
||||||
else:
|
else:
|
||||||
audio = c.answer_av_tags()
|
audio = c.answer_av_tags()
|
||||||
av_player.play_tags(audio)
|
av_player.play_tags(audio)
|
||||||
|
else:
|
||||||
|
av_player.maybe_interrupt()
|
||||||
|
|
||||||
txt = self.mw.prepare_card_text_for_display(txt)
|
txt = self.mw.prepare_card_text_for_display(txt)
|
||||||
txt = gui_hooks.card_will_show(
|
txt = gui_hooks.card_will_show(
|
||||||
|
|
|
@ -11,7 +11,7 @@ from typing import Any
|
||||||
import aqt
|
import aqt
|
||||||
from anki.errors import DeckRenameError
|
from anki.errors import DeckRenameError
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
from anki.rsbackend import FString
|
from anki.rsbackend import TR
|
||||||
from anki.utils import ids2str
|
from anki.utils import ids2str
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -185,7 +185,7 @@ where id > ?""",
|
||||||
<tr><th colspan=5 align=left>%s</th><th class=count>%s</th>
|
<tr><th colspan=5 align=left>%s</th><th class=count>%s</th>
|
||||||
<th class=count>%s</th><th class=optscol></th></tr>""" % (
|
<th class=count>%s</th><th class=optscol></th></tr>""" % (
|
||||||
_("Deck"),
|
_("Deck"),
|
||||||
tr(FString.STATISTICS_DUE_COUNT),
|
tr(TR.STATISTICS_DUE_COUNT),
|
||||||
_("New"),
|
_("New"),
|
||||||
)
|
)
|
||||||
buf += self._topLevelDragRow()
|
buf += self._topLevelDragRow()
|
||||||
|
|
|
@ -11,7 +11,7 @@ from markdown import markdown
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from aqt import mw
|
from aqt import mw
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import FString, showText, showWarning, supportText, tr
|
from aqt.utils import TR, showText, showWarning, supportText, tr
|
||||||
|
|
||||||
if not os.environ.get("DEBUG"):
|
if not os.environ.get("DEBUG"):
|
||||||
|
|
||||||
|
@ -106,14 +106,14 @@ your system's temporary folder may be incorrect."""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if "disk I/O error" in error:
|
if "disk I/O error" in error:
|
||||||
showWarning(markdown(tr(FString.ERRORS_ACCESSING_DB)))
|
showWarning(markdown(tr(TR.ERRORS_ACCESSING_DB)))
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.mw.addonManager.dirty:
|
if self.mw.addonManager.dirty:
|
||||||
txt = markdown(tr(FString.ERRORS_ADDONS_ACTIVE_POPUP))
|
txt = markdown(tr(TR.ERRORS_ADDONS_ACTIVE_POPUP))
|
||||||
error = supportText() + self._addonText(error) + "\n" + error
|
error = supportText() + self._addonText(error) + "\n" + error
|
||||||
else:
|
else:
|
||||||
txt = markdown(tr(FString.ERRORS_STANDARD_POPUP))
|
txt = markdown(tr(TR.ERRORS_STANDARD_POPUP))
|
||||||
error = supportText() + "\n" + error
|
error = supportText() + "\n" + error
|
||||||
|
|
||||||
# show dialog
|
# show dialog
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
@ -15,7 +17,12 @@ from aqt.utils import checkInvalidFilename, getSaveFile, showWarning, tooltip
|
||||||
|
|
||||||
|
|
||||||
class ExportDialog(QDialog):
|
class ExportDialog(QDialog):
|
||||||
def __init__(self, mw, did: Optional[int] = None, cids: Optional[List[int]] = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
mw: aqt.main.AnkiQt,
|
||||||
|
did: Optional[int] = None,
|
||||||
|
cids: Optional[List[int]] = None,
|
||||||
|
):
|
||||||
QDialog.__init__(self, mw, Qt.Window)
|
QDialog.__init__(self, mw, Qt.Window)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.col = mw.col
|
self.col = mw.col
|
||||||
|
@ -118,6 +125,9 @@ class ExportDialog(QDialog):
|
||||||
return
|
return
|
||||||
if checkInvalidFilename(os.path.basename(file), dirsep=False):
|
if checkInvalidFilename(os.path.basename(file), dirsep=False):
|
||||||
continue
|
continue
|
||||||
|
if os.path.commonprefix([self.mw.pm.base, file]) == self.mw.pm.base:
|
||||||
|
showWarning("Please choose a different export location.")
|
||||||
|
continue
|
||||||
break
|
break
|
||||||
self.hide()
|
self.hide()
|
||||||
if file:
|
if file:
|
||||||
|
|
|
@ -862,6 +862,51 @@ class _EditorWillUseFontForFieldFilter:
|
||||||
editor_will_use_font_for_field = _EditorWillUseFontForFieldFilter()
|
editor_will_use_font_for_field = _EditorWillUseFontForFieldFilter()
|
||||||
|
|
||||||
|
|
||||||
|
class _EmptyCardsWillBeDeletedFilter:
|
||||||
|
"""Allow to change the list of cards to delete.
|
||||||
|
|
||||||
|
For example, an add-on creating a method to delete only empty
|
||||||
|
new cards would be done as follow:
|
||||||
|
```
|
||||||
|
from anki.consts import CARD_TYPE_NEW
|
||||||
|
from anki.utils import ids2str
|
||||||
|
from aqt import mw
|
||||||
|
from aqt import gui_hooks
|
||||||
|
|
||||||
|
def filter(cids, col):
|
||||||
|
return col.db.list(
|
||||||
|
f"select id from cards where (type={CARD_TYPE_NEW} and (id in {ids2str(cids)))")
|
||||||
|
|
||||||
|
def emptyNewCard():
|
||||||
|
gui_hooks.append(filter)
|
||||||
|
mw.onEmptyCards()
|
||||||
|
gui_hooks.remove(filter)
|
||||||
|
```"""
|
||||||
|
|
||||||
|
_hooks: List[Callable[[List[int]], List[int]]] = []
|
||||||
|
|
||||||
|
def append(self, cb: Callable[[List[int]], List[int]]) -> None:
|
||||||
|
"""(cids: List[int])"""
|
||||||
|
self._hooks.append(cb)
|
||||||
|
|
||||||
|
def remove(self, cb: Callable[[List[int]], List[int]]) -> None:
|
||||||
|
if cb in self._hooks:
|
||||||
|
self._hooks.remove(cb)
|
||||||
|
|
||||||
|
def __call__(self, cids: List[int]) -> List[int]:
|
||||||
|
for filter in self._hooks:
|
||||||
|
try:
|
||||||
|
cids = filter(cids)
|
||||||
|
except:
|
||||||
|
# if the hook fails, remove it
|
||||||
|
self._hooks.remove(filter)
|
||||||
|
raise
|
||||||
|
return cids
|
||||||
|
|
||||||
|
|
||||||
|
empty_cards_will_be_deleted = _EmptyCardsWillBeDeletedFilter()
|
||||||
|
|
||||||
|
|
||||||
class _MediaSyncDidProgressHook:
|
class _MediaSyncDidProgressHook:
|
||||||
_hooks: List[Callable[["aqt.mediasync.LogEntryWithTime"], None]] = []
|
_hooks: List[Callable[["aqt.mediasync.LogEntryWithTime"], None]] = []
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ def stripSounds(text) -> str:
|
||||||
return aqt.mw.col.backend.strip_av_tags(text)
|
return aqt.mw.col.backend.strip_av_tags(text)
|
||||||
|
|
||||||
|
|
||||||
def fmtTimeSpan(time, pad=0, point=0, inTime=False, unit=99):
|
def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99):
|
||||||
print("fmtTimeSpan() has become col.backend.format_time_span()")
|
print("fmtTimeSpan() has become col.backend.format_time_span()")
|
||||||
return aqt.mw.col.backend.format_time_span(time)
|
return aqt.mw.col.backend.format_time_span(time)
|
||||||
|
|
||||||
|
|
|
@ -1303,6 +1303,7 @@ will be lost. Continue?"""
|
||||||
def onEmptyCards(self):
|
def onEmptyCards(self):
|
||||||
self.progress.start(immediate=True)
|
self.progress.start(immediate=True)
|
||||||
cids = self.col.emptyCids()
|
cids = self.col.emptyCids()
|
||||||
|
cids = gui_hooks.empty_cards_will_be_deleted(cids)
|
||||||
if not cids:
|
if not cids:
|
||||||
self.progress.finish()
|
self.progress.finish()
|
||||||
tooltip(_("No empty cards."))
|
tooltip(_("No empty cards."))
|
||||||
|
|
|
@ -10,13 +10,7 @@ from typing import Iterable, List, Optional, TypeVar
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.rsbackend import (
|
from anki.rsbackend import TR, Interrupted, MediaCheckOutput, Progress, ProgressKind
|
||||||
FString,
|
|
||||||
Interrupted,
|
|
||||||
MediaCheckOutput,
|
|
||||||
Progress,
|
|
||||||
ProgressKind,
|
|
||||||
)
|
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import askUser, restoreGeom, saveGeom, showText, tooltip, tr
|
from aqt.utils import askUser, restoreGeom, saveGeom, showText, tooltip, tr
|
||||||
|
|
||||||
|
@ -89,14 +83,14 @@ class MediaChecker:
|
||||||
layout.addWidget(box)
|
layout.addWidget(box)
|
||||||
|
|
||||||
if output.unused:
|
if output.unused:
|
||||||
b = QPushButton(tr(FString.MEDIA_CHECK_DELETE_UNUSED))
|
b = QPushButton(tr(TR.MEDIA_CHECK_DELETE_UNUSED))
|
||||||
b.setAutoDefault(False)
|
b.setAutoDefault(False)
|
||||||
box.addButton(b, QDialogButtonBox.RejectRole)
|
box.addButton(b, QDialogButtonBox.RejectRole)
|
||||||
b.clicked.connect(lambda c: self._on_trash_files(output.unused)) # type: ignore
|
b.clicked.connect(lambda c: self._on_trash_files(output.unused)) # type: ignore
|
||||||
|
|
||||||
if output.missing:
|
if output.missing:
|
||||||
if any(map(lambda x: x.startswith("latex-"), output.missing)):
|
if any(map(lambda x: x.startswith("latex-"), output.missing)):
|
||||||
b = QPushButton(tr(FString.MEDIA_CHECK_RENDER_LATEX))
|
b = QPushButton(tr(TR.MEDIA_CHECK_RENDER_LATEX))
|
||||||
b.setAutoDefault(False)
|
b.setAutoDefault(False)
|
||||||
box.addButton(b, QDialogButtonBox.RejectRole)
|
box.addButton(b, QDialogButtonBox.RejectRole)
|
||||||
b.clicked.connect(self._on_render_latex) # type: ignore
|
b.clicked.connect(self._on_render_latex) # type: ignore
|
||||||
|
@ -125,17 +119,17 @@ class MediaChecker:
|
||||||
browser.onSearchActivated()
|
browser.onSearchActivated()
|
||||||
showText(err, type="html")
|
showText(err, type="html")
|
||||||
else:
|
else:
|
||||||
tooltip(tr(FString.MEDIA_CHECK_ALL_LATEX_RENDERED))
|
tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED))
|
||||||
|
|
||||||
def _on_render_latex_progress(self, count: int) -> bool:
|
def _on_render_latex_progress(self, count: int) -> bool:
|
||||||
if self.progress_dialog.wantCancel:
|
if self.progress_dialog.wantCancel:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.mw.progress.update(tr(FString.MEDIA_CHECK_CHECKED, count=count))
|
self.mw.progress.update(tr(TR.MEDIA_CHECK_CHECKED, count=count))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _on_trash_files(self, fnames: List[str]):
|
def _on_trash_files(self, fnames: List[str]):
|
||||||
if not askUser(tr(FString.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)):
|
if not askUser(tr(TR.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.progress_dialog = self.mw.progress.start()
|
self.progress_dialog = self.mw.progress.start()
|
||||||
|
@ -149,10 +143,10 @@ class MediaChecker:
|
||||||
remaining -= len(chunk)
|
remaining -= len(chunk)
|
||||||
if time.time() - last_progress >= 0.3:
|
if time.time() - last_progress >= 0.3:
|
||||||
self.mw.progress.update(
|
self.mw.progress.update(
|
||||||
tr(FString.MEDIA_CHECK_FILES_REMAINING, count=remaining)
|
tr(TR.MEDIA_CHECK_FILES_REMAINING, count=remaining)
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
self.progress_dialog = None
|
self.progress_dialog = None
|
||||||
|
|
||||||
tooltip(tr(FString.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total))
|
tooltip(tr(TR.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total))
|
||||||
|
|
|
@ -10,15 +10,7 @@ from typing import List, Union
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.rsbackend import (
|
from anki.rsbackend import TR, Interrupted, MediaSyncProgress, Progress, ProgressKind
|
||||||
FString,
|
|
||||||
Interrupted,
|
|
||||||
MediaSyncProgress,
|
|
||||||
NetworkError,
|
|
||||||
Progress,
|
|
||||||
ProgressKind,
|
|
||||||
SyncError,
|
|
||||||
)
|
|
||||||
from anki.types import assert_impossible
|
from anki.types import assert_impossible
|
||||||
from anki.utils import intTime
|
from anki.utils import intTime
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
|
@ -65,10 +57,10 @@ class MediaSyncer:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.mw.pm.media_syncing_enabled():
|
if not self.mw.pm.media_syncing_enabled():
|
||||||
self._log_and_notify(tr(FString.SYNC_MEDIA_DISABLED))
|
self._log_and_notify(tr(TR.SYNC_MEDIA_DISABLED))
|
||||||
return
|
return
|
||||||
|
|
||||||
self._log_and_notify(tr(FString.SYNC_MEDIA_STARTING))
|
self._log_and_notify(tr(TR.SYNC_MEDIA_STARTING))
|
||||||
self._syncing = True
|
self._syncing = True
|
||||||
self._want_stop = False
|
self._want_stop = False
|
||||||
gui_hooks.media_sync_did_start_or_stop(True)
|
gui_hooks.media_sync_did_start_or_stop(True)
|
||||||
|
@ -101,21 +93,15 @@ class MediaSyncer:
|
||||||
if exc is not None:
|
if exc is not None:
|
||||||
self._handle_sync_error(exc)
|
self._handle_sync_error(exc)
|
||||||
else:
|
else:
|
||||||
self._log_and_notify(tr(FString.SYNC_MEDIA_COMPLETE))
|
self._log_and_notify(tr(TR.SYNC_MEDIA_COMPLETE))
|
||||||
|
|
||||||
def _handle_sync_error(self, exc: BaseException):
|
def _handle_sync_error(self, exc: BaseException):
|
||||||
if isinstance(exc, Interrupted):
|
if isinstance(exc, Interrupted):
|
||||||
self._log_and_notify(tr(FString.SYNC_MEDIA_ABORTED))
|
self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTED))
|
||||||
return
|
return
|
||||||
|
|
||||||
self._log_and_notify(tr(FString.SYNC_MEDIA_FAILED))
|
self._log_and_notify(tr(TR.SYNC_MEDIA_FAILED))
|
||||||
if isinstance(exc, SyncError):
|
showWarning(str(exc))
|
||||||
showWarning(exc.localized())
|
|
||||||
elif isinstance(exc, NetworkError):
|
|
||||||
msg = exc.localized()
|
|
||||||
msg += "\n\n" + tr(FString.NETWORK_DETAILS, details=str(exc))
|
|
||||||
else:
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
def entries(self) -> List[LogEntryWithTime]:
|
def entries(self) -> List[LogEntryWithTime]:
|
||||||
return self._log
|
return self._log
|
||||||
|
@ -123,7 +109,7 @@ class MediaSyncer:
|
||||||
def abort(self) -> None:
|
def abort(self) -> None:
|
||||||
if not self.is_syncing():
|
if not self.is_syncing():
|
||||||
return
|
return
|
||||||
self._log_and_notify(tr(FString.SYNC_MEDIA_ABORTING))
|
self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTING))
|
||||||
self._want_stop = True
|
self._want_stop = True
|
||||||
|
|
||||||
def is_syncing(self) -> bool:
|
def is_syncing(self) -> bool:
|
||||||
|
@ -166,10 +152,12 @@ class MediaSyncDialog(QDialog):
|
||||||
self._close_when_done = close_when_done
|
self._close_when_done = close_when_done
|
||||||
self.form = aqt.forms.synclog.Ui_Dialog()
|
self.form = aqt.forms.synclog.Ui_Dialog()
|
||||||
self.form.setupUi(self)
|
self.form.setupUi(self)
|
||||||
self.abort_button = QPushButton(tr(FString.SYNC_ABORT_BUTTON))
|
self.setWindowTitle(tr(TR.SYNC_MEDIA_LOG_TITLE))
|
||||||
|
self.abort_button = QPushButton(tr(TR.SYNC_ABORT_BUTTON))
|
||||||
self.abort_button.clicked.connect(self._on_abort) # type: ignore
|
self.abort_button.clicked.connect(self._on_abort) # type: ignore
|
||||||
self.abort_button.setAutoDefault(False)
|
self.abort_button.setAutoDefault(False)
|
||||||
self.form.buttonBox.addButton(self.abort_button, QDialogButtonBox.ActionRole)
|
self.form.buttonBox.addButton(self.abort_button, QDialogButtonBox.ActionRole)
|
||||||
|
self.abort_button.setHidden(not self._syncer.is_syncing())
|
||||||
|
|
||||||
gui_hooks.media_sync_did_progress.append(self._on_log_entry)
|
gui_hooks.media_sync_did_progress.append(self._on_log_entry)
|
||||||
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
|
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
|
||||||
|
|
|
@ -10,7 +10,7 @@ import aqt
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from aqt import AnkiQt
|
from aqt import AnkiQt
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import askUser, openHelp, showInfo, showWarning
|
from aqt.utils import TR, askUser, openHelp, showInfo, showWarning, tr
|
||||||
|
|
||||||
|
|
||||||
class Preferences(QDialog):
|
class Preferences(QDialog):
|
||||||
|
@ -177,6 +177,8 @@ class Preferences(QDialog):
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupNetwork(self):
|
def setupNetwork(self):
|
||||||
|
self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON))
|
||||||
|
self.form.media_log.clicked.connect(self.on_media_log)
|
||||||
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
|
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
|
||||||
self.form.syncMedia.setChecked(self.prof["syncMedia"])
|
self.form.syncMedia.setChecked(self.prof["syncMedia"])
|
||||||
if not self.prof["syncKey"]:
|
if not self.prof["syncKey"]:
|
||||||
|
@ -185,6 +187,9 @@ class Preferences(QDialog):
|
||||||
self.form.syncUser.setText(self.prof.get("syncUser", ""))
|
self.form.syncUser.setText(self.prof.get("syncUser", ""))
|
||||||
self.form.syncDeauth.clicked.connect(self.onSyncDeauth)
|
self.form.syncDeauth.clicked.connect(self.onSyncDeauth)
|
||||||
|
|
||||||
|
def on_media_log(self):
|
||||||
|
self.mw.media_syncer.show_sync_log()
|
||||||
|
|
||||||
def _hideAuth(self):
|
def _hideAuth(self):
|
||||||
self.form.syncDeauth.setVisible(False)
|
self.form.syncDeauth.setVisible(False)
|
||||||
self.form.syncUser.setText("")
|
self.form.syncUser.setText("")
|
||||||
|
|
|
@ -485,7 +485,8 @@ please see:
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def uiScale(self) -> float:
|
def uiScale(self) -> float:
|
||||||
return self.meta.get("uiScale", 1.0)
|
scale = self.meta.get("uiScale", 1.0)
|
||||||
|
return max(scale, 1)
|
||||||
|
|
||||||
def setUiScale(self, scale: float) -> None:
|
def setUiScale(self, scale: float) -> None:
|
||||||
self.meta["uiScale"] = scale
|
self.meta["uiScale"] = scale
|
||||||
|
|
|
@ -187,6 +187,8 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||||
# play audio?
|
# play audio?
|
||||||
if self.autoplay(c):
|
if self.autoplay(c):
|
||||||
av_player.play_tags(c.question_av_tags())
|
av_player.play_tags(c.question_av_tags())
|
||||||
|
else:
|
||||||
|
av_player.maybe_interrupt()
|
||||||
|
|
||||||
# render & update bottom
|
# render & update bottom
|
||||||
q = self._mungeQA(q)
|
q = self._mungeQA(q)
|
||||||
|
@ -230,6 +232,8 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||||
# play audio?
|
# play audio?
|
||||||
if self.autoplay(c):
|
if self.autoplay(c):
|
||||||
av_player.play_tags(c.answer_av_tags())
|
av_player.play_tags(c.answer_av_tags())
|
||||||
|
else:
|
||||||
|
av_player.maybe_interrupt()
|
||||||
|
|
||||||
a = self._mungeQA(a)
|
a = self._mungeQA(a)
|
||||||
a = gui_hooks.card_will_show(a, c, "reviewAnswer")
|
a = gui_hooks.card_will_show(a, c, "reviewAnswer")
|
||||||
|
|
|
@ -95,14 +95,17 @@ class AVPlayer:
|
||||||
def play_tags(self, tags: List[AVTag]) -> None:
|
def play_tags(self, tags: List[AVTag]) -> None:
|
||||||
"""Clear the existing queue, then start playing provided tags."""
|
"""Clear the existing queue, then start playing provided tags."""
|
||||||
self._enqueued = tags[:]
|
self._enqueued = tags[:]
|
||||||
if self.interrupt_current_audio:
|
self.maybe_interrupt()
|
||||||
self._stop_if_playing()
|
|
||||||
self._play_next_if_idle()
|
self._play_next_if_idle()
|
||||||
|
|
||||||
def stop_and_clear_queue(self) -> None:
|
def stop_and_clear_queue(self) -> None:
|
||||||
self._enqueued = []
|
self._enqueued = []
|
||||||
self._stop_if_playing()
|
self._stop_if_playing()
|
||||||
|
|
||||||
|
def maybe_interrupt(self) -> None:
|
||||||
|
if self.interrupt_current_audio:
|
||||||
|
self._stop_if_playing()
|
||||||
|
|
||||||
def play_file(self, filename: str) -> None:
|
def play_file(self, filename: str) -> None:
|
||||||
self.play_tags([SoundOrVideoTag(filename=filename)])
|
self.play_tags([SoundOrVideoTag(filename=filename)])
|
||||||
|
|
||||||
|
@ -639,10 +642,6 @@ def setup_audio(taskman: TaskManager, base_folder: str) -> None:
|
||||||
mplayer = SimpleMplayerSlaveModePlayer(taskman)
|
mplayer = SimpleMplayerSlaveModePlayer(taskman)
|
||||||
av_player.players.append(mplayer)
|
av_player.players.append(mplayer)
|
||||||
|
|
||||||
# currently unused
|
|
||||||
# mpv = SimpleMpvPlayer(base_folder)
|
|
||||||
# av_player.players.append(mpv)
|
|
||||||
|
|
||||||
# tts support
|
# tts support
|
||||||
if isMac:
|
if isMac:
|
||||||
from aqt.tts import MacTTSPlayer
|
from aqt.tts import MacTTSPlayer
|
||||||
|
|
|
@ -9,7 +9,7 @@ from typing import Dict
|
||||||
from anki.utils import isMac
|
from anki.utils import isMac
|
||||||
from aqt import QApplication, gui_hooks, isWin
|
from aqt import QApplication, gui_hooks, isWin
|
||||||
from aqt.colors import colors
|
from aqt.colors import colors
|
||||||
from aqt.qt import QColor, QIcon, QPalette, QPixmap, QStyleFactory, Qt
|
from aqt.qt import QColor, QIcon, QPalette, QPixmap, QStyleFactory, Qt, qtminor
|
||||||
|
|
||||||
|
|
||||||
class ThemeManager:
|
class ThemeManager:
|
||||||
|
@ -23,6 +23,8 @@ class ThemeManager:
|
||||||
return False
|
return False
|
||||||
if not isMac:
|
if not isMac:
|
||||||
return False
|
return False
|
||||||
|
if qtminor < 13:
|
||||||
|
return False
|
||||||
import darkdetect # pylint: disable=import-error
|
import darkdetect # pylint: disable=import-error
|
||||||
|
|
||||||
return darkdetect.isDark() is True
|
return darkdetect.isDark() is True
|
||||||
|
|
|
@ -472,9 +472,14 @@ if isWin:
|
||||||
return LCIDS.get(dec_str, "unknown")
|
return LCIDS.get(dec_str, "unknown")
|
||||||
|
|
||||||
class WindowsTTSPlayer(TTSProcessPlayer):
|
class WindowsTTSPlayer(TTSProcessPlayer):
|
||||||
|
try:
|
||||||
speaker = win32com.client.Dispatch("SAPI.SpVoice")
|
speaker = win32com.client.Dispatch("SAPI.SpVoice")
|
||||||
|
except:
|
||||||
|
speaker = None
|
||||||
|
|
||||||
def get_available_voices(self) -> List[TTSVoice]:
|
def get_available_voices(self) -> List[TTSVoice]:
|
||||||
|
if self.speaker is None:
|
||||||
|
return []
|
||||||
return list(map(self._voice_to_object, self.speaker.GetVoices()))
|
return list(map(self._voice_to_object, self.speaker.GetVoices()))
|
||||||
|
|
||||||
def _voice_to_object(self, voice: Any):
|
def _voice_to_object(self, voice: Any):
|
||||||
|
|
|
@ -13,7 +13,7 @@ from typing import Any, Optional, Union
|
||||||
import anki
|
import anki
|
||||||
import aqt
|
import aqt
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from anki.rsbackend import FString
|
from anki.rsbackend import TR
|
||||||
from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild
|
from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.theme import theme_manager
|
from aqt.theme import theme_manager
|
||||||
|
@ -32,7 +32,7 @@ def locale_dir() -> str:
|
||||||
return os.path.join(aqt_data_folder(), "locale")
|
return os.path.join(aqt_data_folder(), "locale")
|
||||||
|
|
||||||
|
|
||||||
def tr(key: FString, **kwargs: Union[str, int, float]) -> str:
|
def tr(key: TR, **kwargs: Union[str, int, float]) -> str:
|
||||||
"Shortcut to access Fluent translations."
|
"Shortcut to access Fluent translations."
|
||||||
return anki.lang.current_i18n.translate(key, **kwargs)
|
return anki.lang.current_i18n.translate(key, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -246,7 +246,7 @@
|
||||||
<string>%</string>
|
<string>%</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="minimum">
|
<property name="minimum">
|
||||||
<number>50</number>
|
<number>100</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>200</number>
|
<number>200</number>
|
||||||
|
@ -371,6 +371,30 @@
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="media_log">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -542,10 +566,10 @@
|
||||||
<tabstop>lrnCutoff</tabstop>
|
<tabstop>lrnCutoff</tabstop>
|
||||||
<tabstop>timeLimit</tabstop>
|
<tabstop>timeLimit</tabstop>
|
||||||
<tabstop>numBackups</tabstop>
|
<tabstop>numBackups</tabstop>
|
||||||
<tabstop>syncOnProgramOpen</tabstop>
|
|
||||||
<tabstop>tabWidget</tabstop>
|
|
||||||
<tabstop>fullSync</tabstop>
|
|
||||||
<tabstop>syncMedia</tabstop>
|
<tabstop>syncMedia</tabstop>
|
||||||
|
<tabstop>tabWidget</tabstop>
|
||||||
|
<tabstop>syncOnProgramOpen</tabstop>
|
||||||
|
<tabstop>fullSync</tabstop>
|
||||||
<tabstop>syncDeauth</tabstop>
|
<tabstop>syncDeauth</tabstop>
|
||||||
</tabstops>
|
</tabstops>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Sync</string>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# build mo files
|
|
||||||
#
|
|
||||||
set -eo pipefail
|
|
||||||
|
|
||||||
targetDir="../aqt_data/locale/gettext"
|
|
||||||
mkdir -p $targetDir
|
|
||||||
|
|
||||||
echo "Compiling *.po..."
|
|
||||||
for file in po/desktop/*/anki.po
|
|
||||||
do
|
|
||||||
outdir=$(echo "$file" | \
|
|
||||||
perl -pe "s%po/desktop/(.*)/anki.po%$targetDir/\1/LC_MESSAGES%")
|
|
||||||
outfile="$outdir/anki.mo"
|
|
||||||
mkdir -p $outdir
|
|
||||||
msgmerge -q "$file" po/desktop/anki.pot | msgfmt - --output-file="$outfile"
|
|
||||||
done
|
|
|
@ -1,11 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [ ! -d po ]; then
|
|
||||||
git clone https://github.com/ankitects/anki-desktop-i18n po
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Updating translations from git..."
|
|
||||||
(cd po && git pull)
|
|
||||||
|
|
||||||
# make sure gettext translations haven't broken something
|
|
||||||
(cd po && python check-po-files.py)
|
|
|
@ -1,9 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -eo pipefail
|
|
||||||
|
|
||||||
# pull any pending changes from git repos
|
|
||||||
./pull-git
|
|
||||||
|
|
||||||
# upload changes to .pot
|
|
||||||
./update-po-template
|
|
||||||
(cd po && git add desktop; git commit -m update; git push)
|
|
2
qt/i18n/.gitignore → qt/po/.gitignore
vendored
2
qt/i18n/.gitignore → qt/po/.gitignore
vendored
|
@ -1,3 +1,3 @@
|
||||||
.build
|
.build
|
||||||
po
|
repo
|
||||||
ftl
|
ftl
|
20
qt/po/scripts/build-mo-files
Executable file
20
qt/po/scripts/build-mo-files
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# build mo files
|
||||||
|
#
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
targetDir="../aqt_data/locale/gettext"
|
||||||
|
mkdir -p $targetDir
|
||||||
|
|
||||||
|
echo "Compiling *.repo..."
|
||||||
|
for file in repo/desktop/*/anki.po
|
||||||
|
do
|
||||||
|
outdir=$(echo "$file" | \
|
||||||
|
perl -pe "s%repo/desktop/(.*)/anki.po%$targetDir/\1/LC_MESSAGES%")
|
||||||
|
outfile="$outdir/anki.mo"
|
||||||
|
mkdir -p $outdir
|
||||||
|
(msgmerge -q "$file" repo/desktop/anki.pot | msgfmt - --output-file="$outfile") || (
|
||||||
|
echo "error building $file"
|
||||||
|
)
|
||||||
|
done
|
9
qt/po/scripts/fetch-latest-translations
Executable file
9
qt/po/scripts/fetch-latest-translations
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Downloading latest translations..."
|
||||||
|
|
||||||
|
if [ ! -d repo ]; then
|
||||||
|
git clone https://github.com/ankitects/anki-desktop-i18n repo
|
||||||
|
fi
|
||||||
|
|
||||||
|
(cd repo && git pull)
|
|
@ -4,7 +4,7 @@
|
||||||
#
|
#
|
||||||
set -eo pipefail
|
set -eo pipefail
|
||||||
|
|
||||||
topDir=$(dirname $0)/../..
|
topDir=$(dirname $0)/../../../
|
||||||
cd $topDir
|
cd $topDir
|
||||||
|
|
||||||
all=all.files
|
all=all.files
|
||||||
|
@ -16,5 +16,5 @@ for i in qt/aqt/{*.py,forms/*.py}; do
|
||||||
echo $i >> $all
|
echo $i >> $all
|
||||||
done
|
done
|
||||||
|
|
||||||
xgettext -cT: -s --no-wrap --files-from=$all --output=qt/i18n/po/desktop/anki.pot
|
xgettext -cT: -s --no-wrap --files-from=$all --output=qt/po/repo/desktop/anki.pot
|
||||||
rm $all
|
rm $all
|
6
qt/po/scripts/upload-latest-template
Executable file
6
qt/po/scripts/upload-latest-template
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
scripts/update-po-template
|
||||||
|
(cd repo && git add desktop; git commit -m update; git push)
|
|
@ -1,13 +1,13 @@
|
||||||
import anki.lang
|
import anki.lang
|
||||||
from anki.rsbackend import FString
|
from anki.rsbackend import TR
|
||||||
|
|
||||||
|
|
||||||
def test_no_collection_i18n():
|
def test_no_collection_i18n():
|
||||||
anki.lang.set_lang("zz", "")
|
anki.lang.set_lang("zz", "")
|
||||||
tr2 = anki.lang.current_i18n.translate
|
tr2 = anki.lang.current_i18n.translate
|
||||||
no_uni = anki.lang.without_unicode_isolation
|
no_uni = anki.lang.without_unicode_isolation
|
||||||
assert no_uni(tr2(FString.STATISTICS_REVIEWS, reviews=2)) == "2 reviews"
|
assert no_uni(tr2(TR.STATISTICS_REVIEWS, reviews=2)) == "2 reviews"
|
||||||
|
|
||||||
anki.lang.set_lang("ja", "")
|
anki.lang.set_lang("ja", "")
|
||||||
tr2 = anki.lang.current_i18n.translate
|
tr2 = anki.lang.current_i18n.translate
|
||||||
assert no_uni(tr2(FString.STATISTICS_REVIEWS, reviews=2)) == "2 枚の復習カード"
|
assert no_uni(tr2(TR.STATISTICS_REVIEWS, reviews=2)) == "2 枚の復習カード"
|
||||||
|
|
|
@ -346,6 +346,30 @@ hooks = [
|
||||||
name="media_sync_did_progress", args=["entry: aqt.mediasync.LogEntryWithTime"],
|
name="media_sync_did_progress", args=["entry: aqt.mediasync.LogEntryWithTime"],
|
||||||
),
|
),
|
||||||
Hook(name="media_sync_did_start_or_stop", args=["running: bool"]),
|
Hook(name="media_sync_did_start_or_stop", args=["running: bool"]),
|
||||||
|
Hook(
|
||||||
|
name="empty_cards_will_be_deleted",
|
||||||
|
args=["cids: List[int]"],
|
||||||
|
return_type="List[int]",
|
||||||
|
doc="""Allow to change the list of cards to delete.
|
||||||
|
|
||||||
|
For example, an add-on creating a method to delete only empty
|
||||||
|
new cards would be done as follow:
|
||||||
|
```
|
||||||
|
from anki.consts import CARD_TYPE_NEW
|
||||||
|
from anki.utils import ids2str
|
||||||
|
from aqt import mw
|
||||||
|
from aqt import gui_hooks
|
||||||
|
|
||||||
|
def filter(cids, col):
|
||||||
|
return col.db.list(
|
||||||
|
f"select id from cards where (type={CARD_TYPE_NEW} and (id in {ids2str(cids)))")
|
||||||
|
|
||||||
|
def emptyNewCard():
|
||||||
|
gui_hooks.append(filter)
|
||||||
|
mw.onEmptyCards()
|
||||||
|
gui_hooks.remove(filter)
|
||||||
|
```""",
|
||||||
|
),
|
||||||
# Adding cards
|
# Adding cards
|
||||||
###################
|
###################
|
||||||
Hook(
|
Hook(
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
/* Copyright: Ankitects Pty Ltd and contributors
|
/* Copyright: Ankitects Pty Ltd and contributors
|
||||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||||
|
|
||||||
|
@use 'vars';
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
}
|
}
|
||||||
|
@ -11,8 +13,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
body.nightMode {
|
body.nightMode {
|
||||||
background-color: black;
|
background-color: vars.$night-window-bg;
|
||||||
color: white;
|
color: vars.$night-text-fg;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
|
|
@ -11,9 +11,16 @@ sync-media-complete = Media sync complete.
|
||||||
sync-media-failed = Media sync failed.
|
sync-media-failed = Media sync failed.
|
||||||
sync-media-aborting = Media sync aborting...
|
sync-media-aborting = Media sync aborting...
|
||||||
sync-media-aborted = Media sync aborted.
|
sync-media-aborted = Media sync aborted.
|
||||||
|
|
||||||
|
# Shown in the sync log to indicate media syncing will not be done, because it
|
||||||
|
# was previously disabled by the user in the preferences screen.
|
||||||
sync-media-disabled = Media sync disabled.
|
sync-media-disabled = Media sync disabled.
|
||||||
|
|
||||||
sync-abort-button = Abort
|
sync-abort-button = Abort
|
||||||
|
sync-media-log-button = Media Log
|
||||||
|
|
||||||
|
# Title of the screen that shows syncing progress history
|
||||||
|
sync-media-log-title = Media Sync Log
|
||||||
|
|
||||||
## Error messages
|
## Error messages
|
||||||
|
|
||||||
|
|
|
@ -44,24 +44,21 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError {
|
||||||
use pb::backend_error::Value as V;
|
use pb::backend_error::Value as V;
|
||||||
let localized = err.localized_description(i18n);
|
let localized = err.localized_description(i18n);
|
||||||
let value = match err {
|
let value = match err {
|
||||||
AnkiError::InvalidInput { info } => V::InvalidInput(pb::StringError { info }),
|
AnkiError::InvalidInput { .. } => V::InvalidInput(pb::Empty {}),
|
||||||
AnkiError::TemplateError { info } => V::TemplateParse(pb::TemplateParseError { info }),
|
AnkiError::TemplateError { .. } => V::TemplateParse(pb::Empty {}),
|
||||||
AnkiError::IOError { info } => V::IoError(pb::StringError { info }),
|
AnkiError::IOError { .. } => V::IoError(pb::Empty {}),
|
||||||
AnkiError::DBError { info } => V::DbError(pb::StringError { info }),
|
AnkiError::DBError { .. } => V::DbError(pb::Empty {}),
|
||||||
AnkiError::NetworkError { info, kind } => V::NetworkError(pb::NetworkError {
|
AnkiError::NetworkError { kind, .. } => {
|
||||||
info,
|
V::NetworkError(pb::NetworkError { kind: kind.into() })
|
||||||
kind: kind.into(),
|
}
|
||||||
localized,
|
AnkiError::SyncError { kind, .. } => V::SyncError(pb::SyncError { kind: kind.into() }),
|
||||||
}),
|
|
||||||
AnkiError::SyncError { info, kind } => V::SyncError(pb::SyncError {
|
|
||||||
info,
|
|
||||||
kind: kind.into(),
|
|
||||||
localized,
|
|
||||||
}),
|
|
||||||
AnkiError::Interrupted => V::Interrupted(Empty {}),
|
AnkiError::Interrupted => V::Interrupted(Empty {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
pb::BackendError { value: Some(value) }
|
pb::BackendError {
|
||||||
|
value: Some(value),
|
||||||
|
localized,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert an Anki error to a protobuf output.
|
// Convert an Anki error to a protobuf output.
|
||||||
|
@ -422,7 +419,10 @@ impl Backend {
|
||||||
None => return "".to_string(),
|
None => return "".to_string(),
|
||||||
};
|
};
|
||||||
match context {
|
match context {
|
||||||
pb::format_time_span_in::Context::Normal => time_span(input.seconds, &self.i18n),
|
pb::format_time_span_in::Context::Precise => time_span(input.seconds, &self.i18n, true),
|
||||||
|
pb::format_time_span_in::Context::Intervals => {
|
||||||
|
time_span(input.seconds, &self.i18n, false)
|
||||||
|
}
|
||||||
pb::format_time_span_in::Context::AnswerButtons => {
|
pb::format_time_span_in::Context::AnswerButtons => {
|
||||||
answer_button_time(input.seconds, &self.i18n)
|
answer_button_time(input.seconds, &self.i18n)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use crate::i18n::{FString, I18n};
|
use crate::i18n::{tr_strs, FString, I18n};
|
||||||
pub use failure::{Error, Fail};
|
pub use failure::{Error, Fail};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
@ -67,14 +67,21 @@ impl AnkiError {
|
||||||
SyncErrorKind::ResyncRequired => i18n.tr(FString::SyncResyncRequired),
|
SyncErrorKind::ResyncRequired => i18n.tr(FString::SyncResyncRequired),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
AnkiError::NetworkError { kind, .. } => match kind {
|
AnkiError::NetworkError { kind, info } => {
|
||||||
|
let summary = match kind {
|
||||||
NetworkErrorKind::Offline => i18n.tr(FString::NetworkOffline),
|
NetworkErrorKind::Offline => i18n.tr(FString::NetworkOffline),
|
||||||
NetworkErrorKind::Timeout => i18n.tr(FString::NetworkTimeout),
|
NetworkErrorKind::Timeout => i18n.tr(FString::NetworkTimeout),
|
||||||
NetworkErrorKind::ProxyAuth => i18n.tr(FString::NetworkProxyAuth),
|
NetworkErrorKind::ProxyAuth => i18n.tr(FString::NetworkProxyAuth),
|
||||||
NetworkErrorKind::Other => i18n.tr(FString::NetworkOther),
|
NetworkErrorKind::Other => i18n.tr(FString::NetworkOther),
|
||||||
|
};
|
||||||
|
let details = i18n.trn(FString::NetworkDetails, tr_strs!["details"=>info]);
|
||||||
|
format!("{}\n\n{}", summary, details)
|
||||||
}
|
}
|
||||||
.into(),
|
AnkiError::TemplateError { info } => {
|
||||||
_ => "".into(),
|
// already localized
|
||||||
|
info.into()
|
||||||
|
}
|
||||||
|
_ => format!("{:?}", self),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,7 @@ use crate::i18n::{tr_args, FString, I18n};
|
||||||
/// Short string like '4d' to place above answer buttons.
|
/// Short string like '4d' to place above answer buttons.
|
||||||
pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String {
|
pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String {
|
||||||
let span = Timespan::from_secs(seconds).natural_span();
|
let span = Timespan::from_secs(seconds).natural_span();
|
||||||
let amount = match span.unit() {
|
let args = tr_args!["amount" => span.as_rounded_unit()];
|
||||||
// months/years shown with 1 decimal place
|
|
||||||
TimespanUnit::Months | TimespanUnit::Years => (span.as_unit() * 10.0).round() / 10.0,
|
|
||||||
// other values shown without decimals
|
|
||||||
_ => span.as_unit().round(),
|
|
||||||
};
|
|
||||||
let args = tr_args!["amount" => amount];
|
|
||||||
let key = match span.unit() {
|
let key = match span.unit() {
|
||||||
TimespanUnit::Seconds => FString::SchedulingAnswerButtonTimeSeconds,
|
TimespanUnit::Seconds => FString::SchedulingAnswerButtonTimeSeconds,
|
||||||
TimespanUnit::Minutes => FString::SchedulingAnswerButtonTimeMinutes,
|
TimespanUnit::Minutes => FString::SchedulingAnswerButtonTimeMinutes,
|
||||||
|
@ -24,11 +18,17 @@ pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String {
|
||||||
i18n.trn(key, args)
|
i18n.trn(key, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Describe the given seconds using the largest appropriate unit
|
/// Describe the given seconds using the largest appropriate unit.
|
||||||
|
/// If precise is true, show to two decimal places, eg
|
||||||
/// eg 70 seconds -> "1.17 minutes"
|
/// eg 70 seconds -> "1.17 minutes"
|
||||||
pub fn time_span(seconds: f32, i18n: &I18n) -> String {
|
/// If false, seconds and days are shown without decimals.
|
||||||
|
pub fn time_span(seconds: f32, i18n: &I18n, precise: bool) -> String {
|
||||||
let span = Timespan::from_secs(seconds).natural_span();
|
let span = Timespan::from_secs(seconds).natural_span();
|
||||||
let amount = span.as_unit();
|
let amount = if precise {
|
||||||
|
span.as_unit()
|
||||||
|
} else {
|
||||||
|
span.as_rounded_unit()
|
||||||
|
};
|
||||||
let args = tr_args!["amount" => amount];
|
let args = tr_args!["amount" => amount];
|
||||||
let key = match span.unit() {
|
let key = match span.unit() {
|
||||||
TimespanUnit::Seconds => FString::SchedulingTimeSpanSeconds,
|
TimespanUnit::Seconds => FString::SchedulingTimeSpanSeconds,
|
||||||
|
@ -133,6 +133,17 @@ impl Timespan {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Round seconds and days to integers, otherwise
|
||||||
|
/// truncates to one decimal place.
|
||||||
|
fn as_rounded_unit(self) -> f32 {
|
||||||
|
match self.unit {
|
||||||
|
// seconds/days as integer
|
||||||
|
TimespanUnit::Seconds | TimespanUnit::Days => self.as_unit().round(),
|
||||||
|
// other values shown to 1 decimal place
|
||||||
|
_ => (self.as_unit() * 10.0).round() / 10.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn unit(self) -> TimespanUnit {
|
fn unit(self) -> TimespanUnit {
|
||||||
self.unit
|
self.unit
|
||||||
}
|
}
|
||||||
|
@ -173,16 +184,18 @@ mod test {
|
||||||
fn answer_buttons() {
|
fn answer_buttons() {
|
||||||
let i18n = I18n::new(&["zz"], "");
|
let i18n = I18n::new(&["zz"], "");
|
||||||
assert_eq!(answer_button_time(30.0, &i18n), "30s");
|
assert_eq!(answer_button_time(30.0, &i18n), "30s");
|
||||||
assert_eq!(answer_button_time(70.0, &i18n), "1m");
|
assert_eq!(answer_button_time(70.0, &i18n), "1.2m");
|
||||||
assert_eq!(answer_button_time(1.1 * MONTH, &i18n), "1.1mo");
|
assert_eq!(answer_button_time(1.1 * MONTH, &i18n), "1.1mo");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn time_spans() {
|
fn time_spans() {
|
||||||
let i18n = I18n::new(&["zz"], "");
|
let i18n = I18n::new(&["zz"], "");
|
||||||
assert_eq!(time_span(1.0, &i18n), "1 second");
|
assert_eq!(time_span(1.0, &i18n, false), "1 second");
|
||||||
assert_eq!(time_span(30.0, &i18n), "30 seconds");
|
assert_eq!(time_span(30.3, &i18n, false), "30 seconds");
|
||||||
assert_eq!(time_span(90.0, &i18n), "1.5 minutes");
|
assert_eq!(time_span(30.3, &i18n, true), "30.3 seconds");
|
||||||
|
assert_eq!(time_span(90.0, &i18n, false), "1.5 minutes");
|
||||||
|
assert_eq!(time_span(45.0 * 86_400.0, &i18n, false), "1.5 months");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Reference in a new issue