Compare commits

...

51 commits

Author SHA1 Message Date
Luc Mcgrady
de5da69897
Merge 1e67a773c6 into 3890e12c9e 2025-09-17 12:35:16 +05:30
Damien Elmes
3890e12c9e Bump version
.1 release skipped due to missing bugfix
2025-09-17 16:50:13 +10:00
llama
80cff16250
fix: persist colour picker's custom palette in profile (#4326)
* add SaveCustomColours rpc method

* restore custom colour palette on editor init

* save custom colour palette on colour picker open and input

there doesn't seem to be an event fired when the picker is
cancelled/closed, so it's still possible for work to be lost

* save colours on `change` instead of `input`

`input` is supposed to be fired on every adjustment to the picker
whereas `change` is only fired when the picker is accepted, but qt
seems to treat both as the latter, so this is currently a no-op

* Store colors in the collection

One minor tweak to the logic while I was there: an invalid color no
longer invalidates all the rest.

---------

Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2025-09-17 16:46:53 +10:00
Abdo
75d9026be5
Decode images asynchronously (#4320) 2025-09-17 09:06:42 +03:00
Damien Elmes
6854d13b88 Bump version 2025-09-17 15:50:16 +10:00
Damien Elmes
29072654db Update translations 2025-09-17 15:50:02 +10:00
jcznk
ec6f09958a
(UI polish) Improved margins in Card Browser's "Previewer" (#4337)
* Improved margins in Card Browser's "Preview" pane

* Alternate approach that looks good on Mac too

---------

Co-authored-by: Damien Elmes <gpg@ankiweb.net>
Co-authored-by: Damien Elmes <dae@users.noreply.github.com>
2025-09-17 15:30:22 +10:00
snowtimeglass
c2957746f4
Make timebox message translatable with flexible variable order (#4338)
* Make timebox message translatable with flexible variable order

Currently, the timebox dialog message is built from two separate strings,
each containing one variable:
"{ $count } cards studied in" + "{ $count } minutes."

As a result, translators cannot freely reorder the variables in their translations.

This change introduces a single string with both variables, allowing translators
to adjust the order for more natural expressions in their languages.

* Preserve old string for now

* Ensure message doesn't display over two lines

---------

Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2025-09-17 15:13:59 +10:00
Luc Mcgrady
9e415869b8
Fix/Add lower review limit to health check. (#4334) 2025-09-17 14:04:27 +10:00
Emil Hamrin
7e8a1076c1
Updated Dockerfile to use Ninja build system (#4321)
* Updated Dockerfile to support ninja build

* Install python using uv

* Bumped python version

* Add disclaimer (dae)
2025-09-17 14:02:09 +10:00
dependabot[bot]
b97fb45e06
Bump vite from 6.3.5 to 6.3.6 (#4328)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-17 12:08:11 +10:00
Damien Elmes
61094d387a Update translations 2025-09-17 09:31:47 +10:00
Damien Elmes
90ed4cc115 Disable NPM package scripts, and assert lockfile unchanged
With all the recent supply chain attacks, this seems prudent. There are
three in our current package list. esbuild's is just a performance
optimization (https://github.com/evanw/esbuild/issues/4085), and
dprint's gets done when we invoke .bin/dprint anyway. svelte-preprocess
simply prints something to the screen.
2025-09-17 09:31:23 +10:00
Luc Mcgrady
1e67a773c6
./check 2025-09-04 00:26:10 +01:00
Luc Mcgrady
c7fd7a0965
re-add reviewer-bottom to build script 2025-09-04 00:25:22 +01:00
Luc Mcgrady
d775efcb06
hide bottomweb 2025-09-03 23:40:37 +01:00
Luc Mcgrady
2e0a75ed83
fix: "card" class included bottombar 2025-09-03 23:38:28 +01:00
Luc Mcgrady
45793b3b64
Added: Todo 2025-09-03 23:36:29 +01:00
Luc Mcgrady
a08bca2673
dumb initial _showQuestion call 2025-09-03 23:24:45 +01:00
Luc Mcgrady
0acc8d14e0
Fix: Card style 2025-09-03 23:14:19 +01:00
Luc Mcgrady
f35b2cf5d2
bodyclass 2025-09-03 23:07:04 +01:00
Luc Mcgrady
d0d1c519e6
Naive reviewer 2025-09-03 22:57:49 +01:00
Luc Mcgrady
eac356139c
Added: Reviewer framework 2025-09-03 22:48:51 +01:00
Luc Mcgrady
7805b1b426
Fix: Wrong function name 2025-09-03 22:34:26 +01:00
Luc Mcgrady
c64dd6c959
Neaten bottom code 2025-09-03 22:29:23 +01:00
Luc Mcgrady
1ec9f4902e
Merge branch 'main' into svelte-reviewer-bottom 2025-09-03 22:08:20 +01:00
Luc Mcgrady
9d3451f97b
fix py issues 2025-09-03 22:07:44 +01:00
Luc Mcgrady
e46d98e2e0
./check 2025-09-03 22:05:35 +01:00
Luc Mcgrady
f01e0f8d0b
toggle svelte reviewer on 2025-09-03 22:01:41 +01:00
Luc Mcgrady
7cb8e62254
revert random rust changes 2025-09-03 21:59:46 +01:00
Luc Mcgrady
d81ec73205
Use inheritance for reviewer 2025-09-03 21:59:34 +01:00
Luc Mcgrady
4bf38ec2af
Added: Reviewer entrypoint 2025-09-03 18:40:37 +01:00
Luc Mcgrady
fac5d64558
Manually specify height 2025-08-26 16:12:03 +01:00
Luc Mcgrady
d49f1eb430
Fix: Update remaining on answer shown 2025-08-26 02:23:23 +01:00
Luc Mcgrady
860a8b4295
./check 2025-08-26 02:08:24 +01:00
Luc Mcgrady
6c540c89f1
./check 2025-08-26 02:03:13 +01:00
Luc Mcgrady
a365369562
align items: center 2025-08-26 01:58:52 +01:00
Luc Mcgrady
28402c548d
Fix: Id not class 2025-08-26 01:58:00 +01:00
Luc Mcgrady
b256e88b1d
Fix: Large font size 2025-08-26 01:56:30 +01:00
Luc Mcgrady
9dbb7abdbb
Remove unneeded globals 2025-08-26 01:46:27 +01:00
Luc Mcgrady
8a57d1c5e1
Fix: showQuestion issues 2025-08-26 01:36:17 +01:00
Luc Mcgrady
992c8ad731
Added: Remaining 2025-08-26 00:45:58 +01:00
Luc Mcgrady
7e92c40169
Added: More bridge command 2025-08-25 23:35:22 +01:00
Luc Mcgrady
f4eb7e0ff9
Use sveltekit 2025-08-25 23:32:33 +01:00
Luc Mcgrady
8c0d1d1720
More i18n 2025-08-25 22:30:28 +01:00
Luc Mcgrady
5d536f2f8e
Added: edit button 2025-08-25 22:01:48 +01:00
Luc Mcgrady
7788aa7785
Answer buttons 2025-08-25 21:55:21 +01:00
Luc Mcgrady
758cfa2693
Buttons template 2025-08-25 21:03:40 +01:00
Luc Mcgrady
34c1dfd849
Added: Svelte component 2025-08-25 20:34:26 +01:00
Luc Mcgrady
244aade836
Fix: Showquestion is not defined 2025-08-25 18:46:35 +01:00
Luc Mcgrady
6869e9fd36
reviewer-bottom entrypoint 2025-08-25 18:42:20 +01:00
32 changed files with 520 additions and 42 deletions

View file

@ -1 +1 @@
25.09 25.09.2

View file

@ -1 +1,2 @@
nodeLinker: node-modules nodeLinker: node-modules
enableScripts: false

View file

@ -49,6 +49,7 @@ Sander Santema <github.com/sandersantema/>
Thomas Brownback <https://github.com/brownbat/> Thomas Brownback <https://github.com/brownbat/>
Andrew Gaul <andrew@gaul.org> Andrew Gaul <andrew@gaul.org>
kenden kenden
Emil Hamrin <github.com/e-hamrin>
Nickolay Yudin <kelciour@gmail.com> Nickolay Yudin <kelciour@gmail.com>
neitrinoweb <github.com/neitrinoweb/> neitrinoweb <github.com/neitrinoweb/>
Andreas Reis <github.com/nwwt> Andreas Reis <github.com/nwwt>

View file

@ -28,7 +28,11 @@ pub fn setup_yarn(args: YarnArgs) {
.arg("--ignore-scripts"), .arg("--ignore-scripts"),
); );
} else { } 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(); std::fs::write(args.stamp, b"").unwrap();

View file

@ -1,35 +1,78 @@
# This Dockerfile uses three stages. # This is a user-contributed Dockerfile. No official support is available.
# 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.
ARG PYTHON_VERSION="3.9"
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
# Build anki. FROM ubuntu:24.04 AS build
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
WORKDIR /opt/anki WORKDIR /opt/anki
COPY . . ENV PYTHON_VERSION="3.13"
# Build python wheels.
# 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 . .
RUN ./tools/build RUN ./tools/build
# Install pre-compiled Anki. # Install pre-compiled Anki.
FROM python:${PYTHON_VERSION}-slim as installer FROM python:3.13-slim AS installer
WORKDIR /opt/anki/ WORKDIR /opt/anki/
COPY --from=build /opt/anki/wheels/ wheels/ COPY --from=build /opt/anki/out/wheels/ wheels/
# Use virtual environment. # Use virtual environment.
RUN python -m venv venv \ 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 setuptools wheel \
&& ./venv/bin/python -m pip install --no-cache-dir /opt/anki/wheels/*.whl && ./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. # 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 COPY --from=installer /opt/anki/venv /opt/anki/venv
ENV PATH=/opt/anki/venv/bin:$PATH ENV PATH=/opt/anki/venv/bin:$PATH
# Install run-time dependencies. # Install run-time dependencies.
@ -59,9 +102,9 @@ RUN apt-get update \
libxrender1 \ libxrender1 \
libxtst6 \ libxtst6 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Add non-root user. # Add non-root user.
RUN useradd --create-home anki RUN useradd --create-home anki
USER anki USER anki
WORKDIR /work WORKDIR /work
ENTRYPOINT ["/opt/anki/venv/bin/anki"] ENTRYPOINT ["/opt/anki/venv/bin/anki"]
LABEL maintainer="Jakub Kaczmarzyk <jakub.kaczmarzyk@gmail.com>"

@ -1 +1 @@
Subproject commit 6552c95a81d162422b2a50126547cc7f1b50c2fd Subproject commit 480ef0da728c7ea3485c58529ae7ee02be3e5dba

View file

@ -46,6 +46,20 @@ studying-type-answer-unknown-field = Type answer: unknown field { $val }
studying-unbury = Unbury studying-unbury = Unbury
studying-what-would-you-like-to-unbury = What would you like to 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-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 = studying-card-studied-in =
{ $count -> { $count ->
[one] { $count } card studied in [one] { $count } card studied in
@ -56,5 +70,3 @@ studying-minute =
[one] { $count } minute. [one] { $count } minute.
*[other] { $count } minutes. *[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

View file

@ -27,6 +27,9 @@ service FrontendService {
rpc deckOptionsRequireClose(generic.Empty) returns (generic.Empty); rpc deckOptionsRequireClose(generic.Empty) returns (generic.Empty);
// Warns python that the deck option web view is ready to receive requests. // Warns python that the deck option web view is ready to receive requests.
rpc deckOptionsReady(generic.Empty) returns (generic.Empty); rpc deckOptionsReady(generic.Empty) returns (generic.Empty);
// Save colour picker's custom colour palette
rpc SaveCustomColours(generic.Empty) returns (generic.Empty);
} }
service BackendFrontendService {} service BackendFrontendService {}

View file

@ -13,7 +13,7 @@ import aqt.browser
from anki.cards import Card from anki.cards import Card
from anki.collection import Config from anki.collection import Config
from anki.tags import MARKED_TAG from anki.tags import MARKED_TAG
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks, is_mac
from aqt.qt import ( from aqt.qt import (
QCheckBox, QCheckBox,
QDialog, QDialog,
@ -81,10 +81,15 @@ class Previewer(QDialog):
qconnect(self.finished, self._on_finished) qconnect(self.finished, self._on_finished)
self.silentlyClose = True self.silentlyClose = True
self.vbox = QVBoxLayout() self.vbox = QVBoxLayout()
spacing = 6
self.vbox.setContentsMargins(0, 0, 0, 0) self.vbox.setContentsMargins(0, 0, 0, 0)
self.vbox.setSpacing(spacing)
self._web: AnkiWebView | None = AnkiWebView(kind=AnkiWebViewKind.PREVIEWER) self._web: AnkiWebView | None = AnkiWebView(kind=AnkiWebViewKind.PREVIEWER)
self.vbox.addWidget(self._web) self.vbox.addWidget(self._web)
self.bbox = QDialogButtonBox() self.bbox = QDialogButtonBox()
self.bbox.setContentsMargins(
spacing, spacing if is_mac else 0, spacing, spacing
)
self.bbox.setLayoutDirection(Qt.LayoutDirection.LeftToRight) self.bbox.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
gui_hooks.card_review_webview_did_init(self._web, AnkiWebViewKind.PREVIEWER) gui_hooks.card_review_webview_did_init(self._web, AnkiWebViewKind.PREVIEWER)

View file

@ -151,6 +151,7 @@ class Editor:
self.add_webview() self.add_webview()
self.setupWeb() self.setupWeb()
self.setupShortcuts() self.setupShortcuts()
self.setupColourPalette()
gui_hooks.editor_did_init(self) gui_hooks.editor_did_init(self)
# Initial setup # Initial setup
@ -349,6 +350,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
keys, fn, _ = row keys, fn, _ = row
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore 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 _addFocusCheck(self, fn: Callable) -> Callable:
def checkFocus() -> None: def checkFocus() -> None:
if self.currentField is None: if self.currentField is None:

View file

@ -1075,9 +1075,9 @@ title="{}" {}>{}</button>""".format(
self.overview = Overview(self) self.overview = Overview(self)
def setupReviewer(self) -> None: def setupReviewer(self) -> None:
from aqt.reviewer import Reviewer from aqt.reviewer import Reviewer, SvelteReviewer
self.reviewer = Reviewer(self) self.reviewer = SvelteReviewer(self) if True else Reviewer(self)
# Syncing # Syncing
########################################################################## ##########################################################################

View file

@ -334,6 +334,7 @@ def is_sveltekit_page(path: str) -> bool:
"import-csv", "import-csv",
"import-page", "import-page",
"image-occlusion", "image-occlusion",
"reviewer",
] ]
@ -599,6 +600,15 @@ def deck_options_ready() -> bytes:
return b"" return b""
def save_custom_colours() -> bytes:
colors = [
QColorDialog.customColor(i).name(QColor.NameFormat.HexArgb)
for i in range(QColorDialog.customCount())
]
aqt.mw.col.set_config("customColorPickerPalette", colors)
return b""
post_handler_list = [ post_handler_list = [
congrats_info, congrats_info,
get_deck_configs_for_update, get_deck_configs_for_update,
@ -614,6 +624,7 @@ post_handler_list = [
search_in_browser, search_in_browser,
deck_options_require_close, deck_options_require_close,
deck_options_ready, deck_options_ready,
save_custom_colours,
] ]

View file

@ -17,6 +17,7 @@ import aqt.browser
import aqt.operations import aqt.operations
from anki.cards import Card, CardId from anki.cards import Card, CardId
from anki.collection import Config, OpChanges, OpChangesWithCount from anki.collection import Config, OpChanges, OpChangesWithCount
from anki.lang import with_collapsed_whitespace
from anki.scheduler.base import ScheduleCardsAsNew from anki.scheduler.base import ScheduleCardsAsNew
from anki.scheduler.v3 import ( from anki.scheduler.v3 import (
CardAnswer, CardAnswer,
@ -966,11 +967,15 @@ timerStopped = false;
elapsed = self.mw.col.timeboxReached() elapsed = self.mw.col.timeboxReached()
if elapsed: if elapsed:
assert not isinstance(elapsed, bool) assert not isinstance(elapsed, bool)
part1 = tr.studying_card_studied_in(count=elapsed[1]) cards_val = elapsed[1]
mins = int(round(elapsed[0] / 60)) minutes_val = int(round(elapsed[0] / 60))
part2 = tr.studying_minute(count=mins) message = with_collapsed_whitespace(
tr.studying_card_studied_in_minute(
cards=cards_val, minutes=str(minutes_val)
)
)
fin = tr.studying_finish() 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) diag.setIcon(QMessageBox.Icon.Information)
if diag.run() == fin: if diag.run() == fin:
self.mw.moveToState("deckBrowser") self.mw.moveToState("deckBrowser")
@ -1228,6 +1233,69 @@ timerStopped = false;
setFlag = set_flag_on_current_card setFlag = set_flag_on_current_card
class SvelteReviewer(Reviewer):
def _answerButtons(self) -> str:
default = self._defaultEase()
assert isinstance(self.mw.col.sched, V3Scheduler)
labels = self.mw.col.sched.describe_next_states(self._v3.states)
def but(i: int, label: str):
if i == default:
id = "defease"
else:
id = ""
due = self._buttonTime(i, v3_labels=labels)
key = (
tr.actions_shortcut_key(val=aqt.mw.pm.get_answer_key(i))
if aqt.mw.pm.get_answer_key(i)
else ""
)
return {
"id": id,
"key": key,
"i": i,
"label": label,
"due": due,
}
return [but(ease, label) for ease, label in self._answerButtonList()] # type: ignore
def _remaining(self) -> str:
if not self.mw.col.conf["dueCounts"]:
return ""
idx, counts = self._v3.counts()
self.bottom.web.eval(f"_updateRemaining({json.dumps(counts)},{idx})")
return ""
def _showAnswerButton(self) -> None:
if self.card.should_show_timer():
maxTime = self.card.time_limit() / 1000
else:
maxTime = 0
self._remaining()
self.bottom.web.eval("_showQuestion(%s,%d);" % ("", maxTime))
def _linkHandler(self, url: str) -> None:
if url == "bottomReady":
self._showQuestion()
self._remaining()
return
super()._linkHandler(url)
def _initWeb(self) -> None:
self._reps = 0
# main window
self.web.load_sveltekit_page("reviewer")
# block default drag & drop behavior while allowing drop events to be received by JS handlers
self.web.allow_drops = True
self.web.eval("_blockDefaultDragDropBehavior();")
# ensure bottom web functions trigger
self.bottom.web = self.web
self.mw.bottomWeb.hide()
# if the last element is a comment, then the RUN_STATE_MUTATION code # if the last element is a comment, then the RUN_STATE_MUTATION code
# breaks due to the comment wrongly commenting out python code. # breaks due to the comment wrongly commenting out python code.
# To prevent this we put the js code on a separate line # To prevent this we put the js code on a separate line

View file

@ -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)?; let fsrs = FSRS::new(None)?;
fsrs.evaluate_with_time_series_splits(input, |_| true) fsrs.evaluate_with_time_series_splits(input, |_| true)
.ok() .ok()

View file

@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import Shortcut from "$lib/components/Shortcut.svelte"; import Shortcut from "$lib/components/Shortcut.svelte";
import { saveCustomColours } from "@generated/backend";
export let keyCombination: string | null = null; export let keyCombination: string | null = null;
export let value: string; 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; let inputRef: HTMLInputElement;
</script> </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} {#if keyCombination}
<Shortcut {keyCombination} on:action={() => inputRef.click()} /> <Shortcut {keyCombination} on:action={() => inputRef.click()} />

View file

@ -19,6 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ColorPicker from "./ColorPicker.svelte"; import ColorPicker from "./ColorPicker.svelte";
import { context as editorToolbarContext } from "./EditorToolbar.svelte"; import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import WithColorHelper from "./WithColorHelper.svelte"; import WithColorHelper from "./WithColorHelper.svelte";
import { saveCustomColours } from "@generated/backend";
export let color: string; 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); color = setColor(event);
bridgeCommand(`lastHighlightColor:${color}`); bridgeCommand(`lastHighlightColor:${color}`);
}} }}
on:change={() => setTextColor()} on:change={() => {
setTextColor();
saveCustomColours({});
}}
/> />
</IconButton> </IconButton>
</WithColorHelper> </WithColorHelper>

View file

@ -22,6 +22,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ColorPicker from "./ColorPicker.svelte"; import ColorPicker from "./ColorPicker.svelte";
import { context as editorToolbarContext } from "./EditorToolbar.svelte"; import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import WithColorHelper from "./WithColorHelper.svelte"; import WithColorHelper from "./WithColorHelper.svelte";
import { saveCustomColours } from "@generated/backend";
export let color: string; export let color: string;
@ -158,6 +159,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
setTimeout(() => { setTimeout(() => {
setTextColor(); setTextColor();
}, 200); }, 200);
saveCustomColours({});
}} }}
/> />
</IconButton> </IconButton>

View file

@ -10,6 +10,9 @@ export function allImagesLoaded(): Promise<void[]> {
} }
function imageLoaded(img: HTMLImageElement): Promise<void> { function imageLoaded(img: HTMLImageElement): Promise<void> {
if (!img.getAttribute("decoding")) {
img.decoding = "async";
}
return img.complete return img.complete
? Promise.resolve() ? Promise.resolve()
: new Promise((resolve) => { : new Promise((resolve) => {

View file

@ -0,0 +1,17 @@
<script>
import ReviewerBottomOuter from "./reviewer-bottom/ReviewerBottomOuter.svelte";
import ReviewerOuter from "./reviewerOuter.svelte";
</script>
<div>
<ReviewerOuter></ReviewerOuter>
<ReviewerBottomOuter></ReviewerBottomOuter>
</div>
<style>
div {
height: calc(100vh - 40px);
display: flex;
flex-direction: column;
}
</style>

View file

View file

@ -0,0 +1,14 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { bridgeCommand } from "@tslib/bridgecommand";
import type { AnswerButtonInfo } from "./types";
export let info: AnswerButtonInfo;
</script>
<button on:click={() => bridgeCommand(`ease${info.i}`)}>
{info.label}
</button>

View file

@ -0,0 +1,16 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
export let underlined: boolean;
export let cls: string;
</script>
<span class={cls}>
{#if underlined}
<u><slot /></u>
{:else}
<slot />
{/if}
</span>

View file

@ -0,0 +1,79 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import type { Writable } from "svelte/store";
import AnswerButton from "./AnswerButton.svelte";
import { bridgeCommand } from "@tslib/bridgecommand";
import * as tr from "@generated/ftl";
import RemainingNumber from "./RemainingNumber.svelte";
import type { AnswerButtonInfo } from "./types";
export let answerButtons: Writable<AnswerButtonInfo[]>;
export let remaining: Writable<number[]>;
export let remainingIndex: Writable<number>;
$: console.log($remaining);
</script>
<div id="outer" class="fancy">
<div id="tableinner">
<div>
<button
title={tr.actionsShortcutKey({ val: "E" })}
on:click={() => bridgeCommand("edit")}
>
{tr.studyingEdit()}
</button>
</div>
<div class="review-buttons">
<span>
<RemainingNumber cls="new-count" underlined={$remainingIndex === 0}>
{$remaining[0]}
</RemainingNumber> +
<RemainingNumber cls="learn-count" underlined={$remainingIndex === 1}>
{$remaining[1]}
</RemainingNumber> +
<RemainingNumber cls="review-count" underlined={$remainingIndex === 2}>
{$remaining[2]}
</RemainingNumber>
</span>
<div>
{#if $answerButtons.length}
{#each $answerButtons as answerButton}
<AnswerButton info={answerButton}></AnswerButton>
{/each}
{:else}
<button on:click={() => bridgeCommand("ans")}>
{tr.studyingShowAnswer()}
</button>
{/if}
</div>
</div>
<div>
<button
on:click={() => bridgeCommand("more")}
title={tr.actionsShortcutKey({ val: "M" })}
>
{tr.studyingMore()}&#8615
</button>
</div>
</div>
</div>
<style lang="scss">
#tableinner {
width: 100%;
display: grid;
grid-template-columns: auto 1fr auto;
justify-items: center;
align-items: center;
}
.review-buttons {
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View file

@ -0,0 +1,20 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount } from "svelte";
import ReviewerBottom from "./ReviewerBottom.svelte";
import "./index.scss";
import { setupBottomBar } from "./reviewer-bottom";
let reviewerInfo: null | ReturnType<typeof setupBottomBar> = null;
onMount(() => {
reviewerInfo = setupBottomBar();
});
</script>
{#if reviewerInfo}
<ReviewerBottom {...reviewerInfo}></ReviewerBottom>
{/if}

View file

@ -1,9 +1,10 @@
/* 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 "../../../../../ts/lib/sass/root-vars"; @use "../../../lib/sass/root-vars";
@use "../../../../../ts/lib/sass/vars" as *; @use "../../../lib/sass/vars" as *;
@use "../../../../../ts/lib/sass/card-counts"; @use "../../../lib/sass/card-counts";
@use "../../../lib/sass/buttons";
:root { :root {
--focus-color: #{palette-of(border-focus)}; --focus-color: #{palette-of(border-focus)};
@ -16,6 +17,8 @@
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-size: 12px;
height: 72px;
} }
#middle td[align="center"] { #middle td[align="center"] {

View file

@ -0,0 +1,90 @@
import { bridgeCommand } from "@tslib/bridgecommand";
import { writable } from "svelte/store";
import type { AnswerButtonInfo } from "./types";
export function setupBottomBar() {
/*
let timerStopped = false;
let maxTime = 0;
function updateTime(): void {
const timeNode = document.getElementById("time");
if (maxTime === 0) {
timeNode.textContent = "";
return;
}
globalThis.time = Math.min(maxTime, globalThis.time);
const m = Math.floor(globalThis.time / 60);
const s = globalThis.time % 60;
const sStr = String(s).padStart(2, "0");
const timeString = `${m}:${sStr}`;
if (maxTime === time) {
timeNode.innerHTML = `<font color=red>${timeString}</font>`;
} else {
timeNode.textContent = timeString;
}
}*/
const answerButtons = writable<AnswerButtonInfo[]>([]);
const remaining = writable<[number, number, number]>([0, 0, 0]);
const remainingIndex = writable<number>(-1);
let intervalId: number | undefined;
function _showQuestion(_txt: string, _maxTime_: number): void {
_showAnswer([]);
globalThis.time = 0;
// maxTime = maxTime_;
// updateTime();
if (intervalId !== undefined) {
clearInterval(intervalId);
}
/*
intervalId = setInterval(function() {
if (!timerStopped) {
globalThis.time += 1;
//updateTime();
}
}, 1000);*/
}
function _showAnswer(info: AnswerButtonInfo[], _stopTimer = false): void {
console.log(info);
answerButtons.set(info);
// timerStopped = stopTimer;
}
function _updateRemaining(counts: [number, number, number], idx: number) {
remaining.set(counts);
remainingIndex.set(idx);
}
globalThis.showQuestion = _showQuestion;
globalThis.showAnswer = _showAnswer;
globalThis._updateRemaining = _updateRemaining;
/*
function selectedAnswerButton(): string | undefined {
const node = document.activeElement as HTMLElement;
if (!node) {
return;
}
return node.dataset.ease;
}
*/
// TODO This should probably be a "ready" command now that it is part of the actual reviewer,
// Currently this depends on this component mounting after the reviewer which it should but seems hacky.
// Maybe use a counter with a counter.subscribe($counter == 2 then call("ready"))
bridgeCommand("bottomReady");
return {
answerButtons,
remaining,
remainingIndex,
};
}

View file

@ -0,0 +1,9 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export interface AnswerButtonInfo {
"extra": string;
"key": string;
"i": number;
"label": string;
"due": string;
}

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { Writable } from "svelte/store";
export let html: Writable<string>;
export let cardClass: Writable<string>;
</script>
<div id="qa" class={$cardClass}>
{@html $html}
</div>
<style lang="scss">
#qa {
flex: 1;
}
</style>

View file

@ -0,0 +1,18 @@
import { writable } from "svelte/store";
import { preloadAnswerImages } from "../../reviewer/images";
export function setupReviewer() {
const html = writable("");
const cardClass = writable("");
function showQuestion(q, a, cc) {
html.set(q);
cardClass.set(cc);
preloadAnswerImages(a);
}
globalThis._showAnswer = html.set;
globalThis._showQuestion = showQuestion;
return { html, cardClass };
}

View file

@ -0,0 +1,21 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount } from "svelte";
import { setupReviewer } from "./reviewer";
import Reviewer from "./reviewer.svelte";
import "../../reviewer/reviewer.scss";
let reviewerInfo: null | ReturnType<typeof setupReviewer> = null;
onMount(() => {
reviewerInfo = setupReviewer();
});
</script>
{#if reviewerInfo}
<Reviewer {...reviewerInfo}></Reviewer>
{/if}

View file

@ -6939,8 +6939,8 @@ __metadata:
linkType: hard linkType: hard
"vite@npm:6": "vite@npm:6":
version: 6.3.5 version: 6.3.6
resolution: "vite@npm:6.3.5" resolution: "vite@npm:6.3.6"
dependencies: dependencies:
esbuild: "npm:^0.25.0" esbuild: "npm:^0.25.0"
fdir: "npm:^6.4.4" fdir: "npm:^6.4.4"
@ -6989,7 +6989,7 @@ __metadata:
optional: true optional: true
bin: bin:
vite: bin/vite.js vite: bin/vite.js
checksum: 10c0/df70201659085133abffc6b88dcdb8a57ef35f742a01311fc56a4cfcda6a404202860729cc65a2c401a724f6e25f9ab40ce4339ed4946f550541531ced6fe41c checksum: 10c0/add701f1e72596c002275782e38d0389ab400c1be330c93a3009804d62db68097a936ca1c53c3301df3aaacfe5e328eab547060f31ef9c49a277ae50df6ad4fb
languageName: node languageName: node
linkType: hard linkType: hard