mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
Compare commits
20 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
436590f4c2 | ||
![]() |
c56e6e55ec | ||
![]() |
04a0b10a15 | ||
![]() |
99c67d39cb | ||
![]() |
0d31c6de4a | ||
![]() |
fb332c4fe1 | ||
![]() |
48f774c711 | ||
![]() |
3890e12c9e | ||
![]() |
80cff16250 | ||
![]() |
75d9026be5 | ||
![]() |
6854d13b88 | ||
![]() |
29072654db | ||
![]() |
ec6f09958a | ||
![]() |
c2957746f4 | ||
![]() |
9e415869b8 | ||
![]() |
7e8a1076c1 | ||
![]() |
b97fb45e06 | ||
![]() |
61094d387a | ||
![]() |
90ed4cc115 | ||
![]() |
4506ad0c97 |
33 changed files with 273 additions and 55 deletions
2
.version
2
.version
|
@ -1 +1 @@
|
|||
25.09
|
||||
25.09.2
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
nodeLinker: node-modules
|
||||
enableScripts: false
|
||||
|
|
|
@ -49,6 +49,7 @@ Sander Santema <github.com/sandersantema/>
|
|||
Thomas Brownback <https://github.com/brownbat/>
|
||||
Andrew Gaul <andrew@gaul.org>
|
||||
kenden
|
||||
Emil Hamrin <github.com/e-hamrin>
|
||||
Nickolay Yudin <kelciour@gmail.com>
|
||||
neitrinoweb <github.com/neitrinoweb/>
|
||||
Andreas Reis <github.com/nwwt>
|
||||
|
|
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -46,9 +46,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
|||
|
||||
[[package]]
|
||||
name = "ammonia"
|
||||
version = "4.1.1"
|
||||
version = "4.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f"
|
||||
checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6"
|
||||
dependencies = [
|
||||
"cssparser",
|
||||
"html5ever 0.35.0",
|
||||
|
|
|
@ -51,7 +51,7 @@ ninja_gen = { "path" = "build/ninja_gen" }
|
|||
unicase = "=2.6.0" # any changes could invalidate sqlite indexes
|
||||
|
||||
# normal
|
||||
ammonia = "4.1.0"
|
||||
ammonia = "4.1.2"
|
||||
anyhow = "1.0.98"
|
||||
async-compression = { version = "0.4.24", features = ["zstd", "tokio"] }
|
||||
async-stream = "0.3.6"
|
||||
|
|
|
@ -28,7 +28,11 @@ pub fn setup_yarn(args: YarnArgs) {
|
|||
.arg("--ignore-scripts"),
|
||||
);
|
||||
} else {
|
||||
run_command(Command::new(&args.yarn_bin).arg("install"));
|
||||
run_command(
|
||||
Command::new(&args.yarn_bin)
|
||||
.arg("install")
|
||||
.arg("--immutable"),
|
||||
);
|
||||
}
|
||||
|
||||
std::fs::write(args.stamp, b"").unwrap();
|
||||
|
|
|
@ -1,35 +1,78 @@
|
|||
# This Dockerfile uses three stages.
|
||||
# 1. Compile anki (and dependencies) and build python wheels.
|
||||
# 2. Create a virtual environment containing anki and its dependencies.
|
||||
# 3. Create a final image that only includes anki's virtual environment and required
|
||||
# system packages.
|
||||
# This is a user-contributed Dockerfile. No official support is available.
|
||||
|
||||
ARG PYTHON_VERSION="3.9"
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
|
||||
# Build anki.
|
||||
FROM python:$PYTHON_VERSION AS build
|
||||
RUN curl -fsSL https://github.com/bazelbuild/bazelisk/releases/download/v1.7.4/bazelisk-linux-amd64 \
|
||||
> /usr/local/bin/bazel \
|
||||
&& chmod +x /usr/local/bin/bazel \
|
||||
# Bazel expects /usr/bin/python
|
||||
&& ln -s /usr/local/bin/python /usr/bin/python
|
||||
FROM ubuntu:24.04 AS build
|
||||
WORKDIR /opt/anki
|
||||
ENV PYTHON_VERSION="3.13"
|
||||
|
||||
|
||||
# System deps
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
git \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libbz2-dev \
|
||||
libreadline-dev \
|
||||
libsqlite3-dev \
|
||||
libffi-dev \
|
||||
zlib1g-dev \
|
||||
liblzma-dev \
|
||||
ca-certificates \
|
||||
ninja-build \
|
||||
rsync \
|
||||
libglib2.0-0 \
|
||||
libgl1 \
|
||||
libx11-6 \
|
||||
libxext6 \
|
||||
libxrender1 \
|
||||
libxkbcommon0 \
|
||||
libxkbcommon-x11-0 \
|
||||
libxcb1 \
|
||||
libxcb-render0 \
|
||||
libxcb-shm0 \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-randr0 \
|
||||
libxcb-shape0 \
|
||||
libxcb-xfixes0 \
|
||||
libxcb-xinerama0 \
|
||||
libxcb-xinput0 \
|
||||
libsm6 \
|
||||
libice6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# install rust with rustup
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
# Install uv and Python 3.13 with uv
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||
&& ln -s /root/.local/bin/uv /usr/local/bin/uv
|
||||
ENV PATH="/root/.local/bin:${PATH}"
|
||||
|
||||
RUN uv python install ${PYTHON_VERSION} --default
|
||||
|
||||
COPY . .
|
||||
# Build python wheels.
|
||||
|
||||
RUN ./tools/build
|
||||
|
||||
|
||||
# Install pre-compiled Anki.
|
||||
FROM python:${PYTHON_VERSION}-slim as installer
|
||||
FROM python:3.13-slim AS installer
|
||||
WORKDIR /opt/anki/
|
||||
COPY --from=build /opt/anki/wheels/ wheels/
|
||||
COPY --from=build /opt/anki/out/wheels/ wheels/
|
||||
# Use virtual environment.
|
||||
RUN python -m venv venv \
|
||||
&& ./venv/bin/python -m pip install --no-cache-dir setuptools wheel \
|
||||
&& ./venv/bin/python -m pip install --no-cache-dir /opt/anki/wheels/*.whl
|
||||
|
||||
|
||||
# We use another build stage here so we don't include the wheels in the final image.
|
||||
FROM python:${PYTHON_VERSION}-slim as final
|
||||
FROM python:3.13-slim AS final
|
||||
COPY --from=installer /opt/anki/venv /opt/anki/venv
|
||||
ENV PATH=/opt/anki/venv/bin:$PATH
|
||||
# Install run-time dependencies.
|
||||
|
@ -59,9 +102,9 @@ RUN apt-get update \
|
|||
libxrender1 \
|
||||
libxtst6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add non-root user.
|
||||
RUN useradd --create-home anki
|
||||
USER anki
|
||||
WORKDIR /work
|
||||
ENTRYPOINT ["/opt/anki/venv/bin/anki"]
|
||||
LABEL maintainer="Jakub Kaczmarzyk <jakub.kaczmarzyk@gmail.com>"
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 6552c95a81d162422b2a50126547cc7f1b50c2fd
|
||||
Subproject commit 480ef0da728c7ea3485c58529ae7ee02be3e5dba
|
|
@ -46,6 +46,20 @@ studying-type-answer-unknown-field = Type answer: unknown field { $val }
|
|||
studying-unbury = Unbury
|
||||
studying-what-would-you-like-to-unbury = What would you like to unbury?
|
||||
studying-you-havent-recorded-your-voice-yet = You haven't recorded your voice yet.
|
||||
studying-card-studied-in-minute =
|
||||
{ $cards ->
|
||||
[one] { $cards } card
|
||||
*[other] { $cards } cards
|
||||
} studied in
|
||||
{ $minutes ->
|
||||
[one] { $minutes } minute.
|
||||
*[other] { $minutes } minutes.
|
||||
}
|
||||
studying-question-time-elapsed = Question time elapsed
|
||||
studying-answer-time-elapsed = Answer time elapsed
|
||||
|
||||
## OBSOLETE; you do not need to translate this
|
||||
|
||||
studying-card-studied-in =
|
||||
{ $count ->
|
||||
[one] { $count } card studied in
|
||||
|
@ -56,5 +70,3 @@ studying-minute =
|
|||
[one] { $count } minute.
|
||||
*[other] { $count } minutes.
|
||||
}
|
||||
studying-question-time-elapsed = Question time elapsed
|
||||
studying-answer-time-elapsed = Answer time elapsed
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit dad4e2736a2b53dcdb52d79b5703dd464c05d666
|
||||
Subproject commit fd5f984785ad07a0d3dbd893ee3d7e3671eaebd6
|
|
@ -20,6 +20,7 @@ service CollectionService {
|
|||
rpc LatestProgress(generic.Empty) returns (Progress);
|
||||
rpc SetWantsAbort(generic.Empty) returns (generic.Empty);
|
||||
rpc SetLoadBalancerEnabled(generic.Bool) returns (OpChanges);
|
||||
rpc GetCustomColours(generic.Empty) returns (GetCustomColoursResponse);
|
||||
}
|
||||
|
||||
// Implicitly includes any of the above methods that are not listed in the
|
||||
|
@ -163,3 +164,7 @@ message CreateBackupRequest {
|
|||
bool force = 2;
|
||||
bool wait_for_completion = 3;
|
||||
}
|
||||
|
||||
message GetCustomColoursResponse {
|
||||
repeated string colours = 1;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,9 @@ service FrontendService {
|
|||
rpc deckOptionsRequireClose(generic.Empty) returns (generic.Empty);
|
||||
// Warns python that the deck option web view is ready to receive requests.
|
||||
rpc deckOptionsReady(generic.Empty) returns (generic.Empty);
|
||||
|
||||
// Save colour picker's custom colour palette
|
||||
rpc SaveCustomColours(generic.Empty) returns (generic.Empty);
|
||||
}
|
||||
|
||||
service BackendFrontendService {}
|
||||
|
|
|
@ -18,7 +18,7 @@ from anki._legacy import DeprecatedNamesMixinForModule
|
|||
TR = anki._fluent.LegacyTranslationEnum
|
||||
FormatTimeSpan = _pb.FormatTimespanRequest
|
||||
|
||||
|
||||
# When adding new languages here, check lang_to_disk_lang() below
|
||||
langs = sorted(
|
||||
[
|
||||
("Afrikaans", "af_ZA"),
|
||||
|
@ -38,6 +38,7 @@ langs = sorted(
|
|||
("Italiano", "it_IT"),
|
||||
("lo jbobau", "jbo_EN"),
|
||||
("Lenga d'òc", "oc_FR"),
|
||||
("Қазақша", "kk_KZ"),
|
||||
("Magyar", "hu_HU"),
|
||||
("Nederlands", "nl_NL"),
|
||||
("Norsk", "nb_NO"),
|
||||
|
@ -64,6 +65,7 @@ langs = sorted(
|
|||
("Українська мова", "uk_UA"),
|
||||
("Հայերեն", "hy_AM"),
|
||||
("עִבְרִית", "he_IL"),
|
||||
("ייִדיש", "yi"),
|
||||
("العربية", "ar_SA"),
|
||||
("فارسی", "fa_IR"),
|
||||
("ภาษาไทย", "th_TH"),
|
||||
|
@ -104,6 +106,7 @@ compatMap = {
|
|||
"it": "it_IT",
|
||||
"ja": "ja_JP",
|
||||
"jbo": "jbo_EN",
|
||||
"kk": "kk_KZ",
|
||||
"ko": "ko_KR",
|
||||
"la": "la_LA",
|
||||
"mn": "mn_MN",
|
||||
|
@ -126,6 +129,7 @@ compatMap = {
|
|||
"uk": "uk_UA",
|
||||
"uz": "uz_UZ",
|
||||
"vi": "vi_VN",
|
||||
"yi": "yi",
|
||||
}
|
||||
|
||||
|
||||
|
@ -233,7 +237,7 @@ def get_def_lang(user_lang: str | None = None) -> tuple[int, str]:
|
|||
|
||||
|
||||
def is_rtl(lang: str) -> bool:
|
||||
return lang in ("he", "ar", "fa", "ug")
|
||||
return lang in ("he", "ar", "fa", "ug", "yi")
|
||||
|
||||
|
||||
# strip off unicode isolation markers from a translated string
|
||||
|
|
|
@ -32,6 +32,7 @@ def test_find_cards():
|
|||
note = col.newNote()
|
||||
note["Front"] = "cat"
|
||||
note["Back"] = "sheep"
|
||||
note.tags.append("conjunção größte")
|
||||
col.addNote(note)
|
||||
catCard = note.cards()[0]
|
||||
m = col.models.current()
|
||||
|
@ -68,6 +69,8 @@ def test_find_cards():
|
|||
col.tags.bulk_remove(col.db.list("select id from notes"), "foo")
|
||||
assert len(col.find_cards("tag:foo")) == 0
|
||||
assert len(col.find_cards("tag:bar")) == 5
|
||||
assert len(col.find_cards("tag:conjuncao tag:groste")) == 0
|
||||
assert len(col.find_cards("tag:nc:conjuncao tag:nc:groste")) == 1
|
||||
# text searches
|
||||
assert len(col.find_cards("cat")) == 2
|
||||
assert len(col.find_cards("cat -dog")) == 1
|
||||
|
|
|
@ -226,6 +226,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
|||
"Anon_0000",
|
||||
"Bilolbek Normuminov",
|
||||
"Sagiv Marzini",
|
||||
"Zhanibek Rassululy",
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import aqt.browser
|
|||
from anki.cards import Card
|
||||
from anki.collection import Config
|
||||
from anki.tags import MARKED_TAG
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
from aqt import AnkiQt, gui_hooks, is_mac
|
||||
from aqt.qt import (
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
|
@ -81,10 +81,15 @@ class Previewer(QDialog):
|
|||
qconnect(self.finished, self._on_finished)
|
||||
self.silentlyClose = True
|
||||
self.vbox = QVBoxLayout()
|
||||
spacing = 6
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.vbox.setSpacing(spacing)
|
||||
self._web: AnkiWebView | None = AnkiWebView(kind=AnkiWebViewKind.PREVIEWER)
|
||||
self.vbox.addWidget(self._web)
|
||||
self.bbox = QDialogButtonBox()
|
||||
self.bbox.setContentsMargins(
|
||||
spacing, spacing if is_mac else 0, spacing, spacing
|
||||
)
|
||||
self.bbox.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
|
||||
|
||||
gui_hooks.card_review_webview_did_init(self._web, AnkiWebViewKind.PREVIEWER)
|
||||
|
|
|
@ -151,6 +151,7 @@ class Editor:
|
|||
self.add_webview()
|
||||
self.setupWeb()
|
||||
self.setupShortcuts()
|
||||
self.setupColourPalette()
|
||||
gui_hooks.editor_did_init(self)
|
||||
|
||||
# Initial setup
|
||||
|
@ -349,6 +350,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
keys, fn, _ = row
|
||||
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore
|
||||
|
||||
def setupColourPalette(self) -> None:
|
||||
if not (colors := self.mw.col.get_config("customColorPickerPalette")):
|
||||
return
|
||||
for i, colour in enumerate(colors[: QColorDialog.customCount()]):
|
||||
if not QColor.isValidColorName(colour):
|
||||
continue
|
||||
QColorDialog.setCustomColor(i, QColor.fromString(colour))
|
||||
|
||||
def _addFocusCheck(self, fn: Callable) -> Callable:
|
||||
def checkFocus() -> None:
|
||||
if self.currentField is None:
|
||||
|
|
|
@ -170,13 +170,42 @@ def favicon() -> Response:
|
|||
|
||||
def _mime_for_path(path: str) -> str:
|
||||
"Mime type for provided path/filename."
|
||||
if path.endswith(".css"):
|
||||
# some users may have invalid mime type in the Windows registry
|
||||
return "text/css"
|
||||
elif path.endswith(".js") or path.endswith(".mjs"):
|
||||
return "application/javascript"
|
||||
|
||||
_, ext = os.path.splitext(path)
|
||||
ext = ext.lower()
|
||||
|
||||
# Badly-behaved apps on Windows can alter the standard mime types in the registry, which can completely
|
||||
# break Anki's UI. So we hard-code the most common extensions.
|
||||
mime_types = {
|
||||
".css": "text/css",
|
||||
".js": "application/javascript",
|
||||
".mjs": "application/javascript",
|
||||
".html": "text/html",
|
||||
".htm": "text/html",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".ico": "image/x-icon",
|
||||
".json": "application/json",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".ttf": "font/ttf",
|
||||
".otf": "font/otf",
|
||||
".mp3": "audio/mpeg",
|
||||
".mp4": "video/mp4",
|
||||
".webm": "video/webm",
|
||||
".ogg": "audio/ogg",
|
||||
".pdf": "application/pdf",
|
||||
".txt": "text/plain",
|
||||
}
|
||||
|
||||
if mime := mime_types.get(ext):
|
||||
return mime
|
||||
else:
|
||||
# autodetect
|
||||
# fallback to mimetypes, which may consult the registry
|
||||
mime, _encoding = mimetypes.guess_type(path)
|
||||
return mime or "application/octet-stream"
|
||||
|
||||
|
@ -599,6 +628,15 @@ def deck_options_ready() -> bytes:
|
|||
return b""
|
||||
|
||||
|
||||
def save_custom_colours() -> bytes:
|
||||
colors = [
|
||||
QColorDialog.customColor(i).name(QColor.NameFormat.HexRgb)
|
||||
for i in range(QColorDialog.customCount())
|
||||
]
|
||||
aqt.mw.col.set_config("customColorPickerPalette", colors)
|
||||
return b""
|
||||
|
||||
|
||||
post_handler_list = [
|
||||
congrats_info,
|
||||
get_deck_configs_for_update,
|
||||
|
@ -614,12 +652,14 @@ post_handler_list = [
|
|||
search_in_browser,
|
||||
deck_options_require_close,
|
||||
deck_options_ready,
|
||||
save_custom_colours,
|
||||
]
|
||||
|
||||
|
||||
exposed_backend_list = [
|
||||
# CollectionService
|
||||
"latest_progress",
|
||||
"get_custom_colours",
|
||||
# DeckService
|
||||
"get_deck_names",
|
||||
# I18nService
|
||||
|
|
|
@ -17,6 +17,7 @@ import aqt.browser
|
|||
import aqt.operations
|
||||
from anki.cards import Card, CardId
|
||||
from anki.collection import Config, OpChanges, OpChangesWithCount
|
||||
from anki.lang import with_collapsed_whitespace
|
||||
from anki.scheduler.base import ScheduleCardsAsNew
|
||||
from anki.scheduler.v3 import (
|
||||
CardAnswer,
|
||||
|
@ -966,11 +967,15 @@ timerStopped = false;
|
|||
elapsed = self.mw.col.timeboxReached()
|
||||
if elapsed:
|
||||
assert not isinstance(elapsed, bool)
|
||||
part1 = tr.studying_card_studied_in(count=elapsed[1])
|
||||
mins = int(round(elapsed[0] / 60))
|
||||
part2 = tr.studying_minute(count=mins)
|
||||
cards_val = elapsed[1]
|
||||
minutes_val = int(round(elapsed[0] / 60))
|
||||
message = with_collapsed_whitespace(
|
||||
tr.studying_card_studied_in_minute(
|
||||
cards=cards_val, minutes=str(minutes_val)
|
||||
)
|
||||
)
|
||||
fin = tr.studying_finish()
|
||||
diag = askUserDialog(f"{part1} {part2}", [tr.studying_continue(), fin])
|
||||
diag = askUserDialog(message, [tr.studying_continue(), fin])
|
||||
diag.setIcon(QMessageBox.Icon.Information)
|
||||
if diag.run() == fin:
|
||||
self.mw.moveToState("deckBrowser")
|
||||
|
|
|
@ -180,7 +180,7 @@ class CustomStyles:
|
|||
QPushButton {{
|
||||
margin: 1px;
|
||||
}}
|
||||
QPushButton:focus {{
|
||||
QPushButton:focus, QPushButton:default:hover {{
|
||||
border: 2px solid {tm.var(colors.BORDER_FOCUS)};
|
||||
outline: none;
|
||||
margin: 0px;
|
||||
|
@ -199,9 +199,6 @@ class CustomStyles:
|
|||
)
|
||||
};
|
||||
}}
|
||||
QPushButton:default:hover {{
|
||||
border-width: 2px;
|
||||
}}
|
||||
QPushButton:pressed,
|
||||
QPushButton:checked,
|
||||
QSpinBox::up-button:pressed,
|
||||
|
|
|
@ -134,5 +134,8 @@ pub fn ensure_os_supported() -> Result<()> {
|
|||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
unix::ensure_glibc_supported()?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
windows::ensure_windows_version_supported()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -38,6 +38,26 @@ fn is_windows_10() -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ensures Windows 10 version 1809 or later
|
||||
pub fn ensure_windows_version_supported() -> Result<()> {
|
||||
unsafe {
|
||||
let mut info = OSVERSIONINFOW {
|
||||
dwOSVersionInfoSize: std::mem::size_of::<OSVERSIONINFOW>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if RtlGetVersion(&mut info).is_err() {
|
||||
anyhow::bail!("Failed to get Windows version information");
|
||||
}
|
||||
|
||||
if info.dwBuildNumber >= 17763 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
anyhow::bail!("Windows 10 version 1809 or later is required.")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_terminal_shown() -> Result<()> {
|
||||
unsafe {
|
||||
if !GetConsoleWindow().is_invalid() {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
use anki_proto::collection::GetCustomColoursResponse;
|
||||
use anki_proto::generic;
|
||||
|
||||
use crate::collection::Collection;
|
||||
use crate::config::ConfigKey;
|
||||
use crate::error;
|
||||
use crate::prelude::BoolKey;
|
||||
use crate::prelude::Op;
|
||||
|
@ -62,4 +64,13 @@ impl crate::services::CollectionService for Collection {
|
|||
})
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn get_custom_colours(
|
||||
&mut self,
|
||||
) -> error::Result<anki_proto::collection::GetCustomColoursResponse> {
|
||||
let colours = self
|
||||
.get_config_optional(ConfigKey::CustomColorPickerPalette)
|
||||
.unwrap_or_default();
|
||||
Ok(GetCustomColoursResponse { colours })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,6 +71,7 @@ pub(crate) enum ConfigKey {
|
|||
NextNewCardPosition,
|
||||
#[strum(to_string = "schedVer")]
|
||||
SchedulerVersion,
|
||||
CustomColorPickerPalette,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
|
||||
|
|
|
@ -174,7 +174,7 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
|
||||
let health_check_passed = if health_check {
|
||||
let health_check_passed = if health_check && input.train_set.len() > 300 {
|
||||
let fsrs = FSRS::new(None)?;
|
||||
fsrs.evaluate_with_time_series_splits(input, |_| true)
|
||||
.ok()
|
||||
|
|
|
@ -392,6 +392,11 @@ fn parse_tag(s: &str) -> ParseResult<'_, SearchNode> {
|
|||
tag: unescape_quotes(re),
|
||||
mode: FieldSearchMode::Regex,
|
||||
}
|
||||
} else if let Some(nc) = s.strip_prefix("nc:") {
|
||||
SearchNode::Tag {
|
||||
tag: unescape(nc)?,
|
||||
mode: FieldSearchMode::NoCombining,
|
||||
}
|
||||
} else {
|
||||
SearchNode::Tag {
|
||||
tag: unescape(s)?,
|
||||
|
|
|
@ -311,8 +311,19 @@ impl SqlWriter<'_> {
|
|||
}
|
||||
s if s.contains(' ') => write!(self.sql, "false").unwrap(),
|
||||
text => {
|
||||
let text = if mode == FieldSearchMode::Normal {
|
||||
write!(self.sql, "n.tags regexp ?").unwrap();
|
||||
let re = &to_custom_re(text, r"\S");
|
||||
Cow::from(text)
|
||||
} else {
|
||||
write!(
|
||||
self.sql,
|
||||
"coalesce(process_text(n.tags, {}), n.tags) regexp ?",
|
||||
ProcessTextFlags::NoCombining.bits()
|
||||
)
|
||||
.unwrap();
|
||||
without_combining(text)
|
||||
};
|
||||
let re = &to_custom_re(&text, r"\S");
|
||||
self.args.push(format!("(?i).* {re}(::| ).*"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import Shortcut from "$lib/components/Shortcut.svelte";
|
||||
import { saveCustomColours } from "@generated/backend";
|
||||
|
||||
export let keyCombination: string | null = null;
|
||||
export let value: string;
|
||||
|
@ -11,7 +12,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let inputRef: HTMLInputElement;
|
||||
</script>
|
||||
|
||||
<input bind:this={inputRef} tabindex="-1" type="color" bind:value on:input on:change />
|
||||
<input
|
||||
bind:this={inputRef}
|
||||
tabindex="-1"
|
||||
type="color"
|
||||
bind:value
|
||||
on:input
|
||||
on:change
|
||||
on:click={() => saveCustomColours({})}
|
||||
/>
|
||||
|
||||
{#if keyCombination}
|
||||
<Shortcut {keyCombination} on:action={() => inputRef.click()} />
|
||||
|
|
|
@ -19,6 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import ColorPicker from "./ColorPicker.svelte";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import WithColorHelper from "./WithColorHelper.svelte";
|
||||
import { saveCustomColours } from "@generated/backend";
|
||||
|
||||
export let color: string;
|
||||
|
||||
|
@ -134,7 +135,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
color = setColor(event);
|
||||
bridgeCommand(`lastHighlightColor:${color}`);
|
||||
}}
|
||||
on:change={() => setTextColor()}
|
||||
on:change={() => {
|
||||
setTextColor();
|
||||
saveCustomColours({});
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</WithColorHelper>
|
||||
|
|
|
@ -22,6 +22,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import ColorPicker from "./ColorPicker.svelte";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import WithColorHelper from "./WithColorHelper.svelte";
|
||||
import { saveCustomColours } from "@generated/backend";
|
||||
|
||||
export let color: string;
|
||||
|
||||
|
@ -158,6 +159,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
setTimeout(() => {
|
||||
setTextColor();
|
||||
}, 200);
|
||||
saveCustomColours({});
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
|
|
@ -10,6 +10,9 @@ export function allImagesLoaded(): Promise<void[]> {
|
|||
}
|
||||
|
||||
function imageLoaded(img: HTMLImageElement): Promise<void> {
|
||||
if (!img.getAttribute("decoding")) {
|
||||
img.decoding = "async";
|
||||
}
|
||||
return img.complete
|
||||
? Promise.resolve()
|
||||
: new Promise((resolve) => {
|
||||
|
|
|
@ -55,6 +55,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
onWheelDragX,
|
||||
} from "./tools/tool-zoom";
|
||||
import { fillMask } from "./tools/tool-fill";
|
||||
import { getCustomColours, saveCustomColours } from "@generated/backend";
|
||||
|
||||
export let canvas;
|
||||
export let iconSize;
|
||||
|
@ -76,6 +77,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let colourRef: HTMLInputElement | undefined;
|
||||
const colour = writable(SHAPE_MASK_COLOR);
|
||||
|
||||
const customColorPickerPalette = writable<string[]>([]);
|
||||
|
||||
async function loadCustomColours() {
|
||||
customColorPickerPalette.set(
|
||||
(await getCustomColours({})).colours.filter(
|
||||
(hex) => !hex.startsWith("#ffffff"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
const upperCanvas = document.querySelector(".upper-canvas");
|
||||
if (event.target == upperCanvas) {
|
||||
|
@ -233,6 +244,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
on(document, "touchstart", onTouchstart),
|
||||
on(document, "mousemove", onMousemoveDocument),
|
||||
);
|
||||
loadCustomColours();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
|
@ -241,7 +253,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<datalist id="colour-palette">
|
||||
<option value={SHAPE_MASK_COLOR}></option>
|
||||
<option>{SHAPE_MASK_COLOR}</option>
|
||||
{#each $customColorPickerPalette as colour}
|
||||
<option>{colour}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
|
||||
<input
|
||||
|
@ -251,6 +266,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
list="colour-palette"
|
||||
value={SHAPE_MASK_COLOR}
|
||||
on:input={(e) => ($colour = e.currentTarget!.value)}
|
||||
on:change={() => saveCustomColours({})}
|
||||
/>
|
||||
|
||||
<div class="tool-bar-container" style:--fill-tool-colour={$colour}>
|
||||
|
|
|
@ -6939,8 +6939,8 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"vite@npm:6":
|
||||
version: 6.3.5
|
||||
resolution: "vite@npm:6.3.5"
|
||||
version: 6.3.6
|
||||
resolution: "vite@npm:6.3.6"
|
||||
dependencies:
|
||||
esbuild: "npm:^0.25.0"
|
||||
fdir: "npm:^6.4.4"
|
||||
|
@ -6989,7 +6989,7 @@ __metadata:
|
|||
optional: true
|
||||
bin:
|
||||
vite: bin/vite.js
|
||||
checksum: 10c0/df70201659085133abffc6b88dcdb8a57ef35f742a01311fc56a4cfcda6a404202860729cc65a2c401a724f6e25f9ab40ce4339ed4946f550541531ced6fe41c
|
||||
checksum: 10c0/add701f1e72596c002275782e38d0389ab400c1be330c93a3009804d62db68097a936ca1c53c3301df3aaacfe5e328eab547060f31ef9c49a277ae50df6ad4fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Reference in a new issue