Compare commits

...

12 commits

Author SHA1 Message Date
Damien Elmes
ccd9ca1a83 Fix casing of 'historical retention'
(cherry picked from commit 1780c89d78)
2024-04-11 15:14:27 +07:00
Damien Elmes
6643b1bc58 fix select all and change ordinal in edit mode in io (#3109)
* fix select all and change ordinal in edit mode in io

* make ordinal undefined for all shapes in group/ungroup

* fix group shapes and some ui fixes

* Don't add node_modules/* to dprint deps

* use minimum ordinal when shape merged, use max ordinal++ when ungrouped, in add mode no ordinal preset so NaN

* use state for ungroup shape

* maintain existing ordinal in editing mode

* fix order of ordinal in ungroup shape

* refactor: remove for loop, use forEach

(cherry picked from commit 477f932f35)
2024-04-11 15:14:25 +07:00
Antoine Q
b8e7dd060f Update deck-config.ftl to clarify what optimal retention means (#3129)
* Update deck-config.ftl to clarify what optimal retention means

Renaming “Optimal retention” to “Minimum recommended retention"

* Update deck-config.ftl

Removing "Predicted" in deck-config-predicted-optimal-retention

* Update deck-config.ftl

Updating keys

* Update ftl/core/deck-config.ftl

Co-authored-by: Damien Elmes <dae@users.noreply.github.com>
(cherry picked from commit da4551e351)
2024-04-11 15:04:11 +07:00
Abdo
e72fec18ae Fix IO text masks being grouped (#3128)
(cherry picked from commit 29bdc47482)
2024-04-11 15:03:55 +07:00
Abdo
ca24eed1ac Fix IO mask position slightly off in edit mode (#3121)
(cherry picked from commit 642f29cf5e)
2024-04-11 15:03:31 +07:00
Escape0707
4daf293d91 Ignore exception raised by pip_system_certs not found (#3114)
This dependency usually doesn't benefit Linux distros with requests library configured to use system certificate already. And is not packaged by most distros. Making it optional will make most Linux users' installation much easier.

(cherry picked from commit 97efd49cd8)
2024-04-11 15:02:25 +07:00
Damien Elmes
1e50172caf Another attempt at fixing rounding issue with optimal retention
Also use the previously-added translation.

(cherry picked from commit 3033b54890)
2024-04-11 15:01:11 +07:00
Damien Elmes
88a67b700c Forgot->Reset
(cherry picked from commit 20aff51df8)
2024-04-11 15:00:41 +07:00
Antoine Q
5ad68fc3bf Update about.py (#3112)
Update about.py to add my name.

(cherry picked from commit 068f14378e)
2024-04-11 15:00:03 +07:00
Damien Elmes
ab8b75298f Bump version 2024-04-03 15:18:55 +07:00
Damien Elmes
5fa2693677 Ensure ankihelper is rebuilt on arch change
https://forums.ankiweb.net/t/24-04-breaks-dark-mode-on-mac/43048
(cherry picked from commit bdc9be2bbb)
2024-04-03 15:15:29 +07:00
Damien Elmes
f638f8c5b7 Possible fix for crash on first sync
https://forums.ankiweb.net/t/desktop-app-closes-itself-after-i-try-to-log-in/43132
(cherry picked from commit a5154635dc)
2024-04-03 15:15:17 +07:00
18 changed files with 131 additions and 54 deletions

View file

@ -1 +1 @@
24.04
24.04.1

View file

@ -335,6 +335,7 @@ fn build_macos_helper(build: &mut Build) -> Result<()> {
inputs: hashmap! {
"script" => inputs!["qt/mac/helper_build.py"],
"in" => inputs![glob!["qt/mac/*.swift"]],
"" => inputs!["out/env"],
},
outputs: hashmap! {
"out" => vec!["qt/_aqt/data/lib/libankihelper.dylib"],

View file

@ -458,7 +458,7 @@ fn build_and_check_reviewer(build: &mut Build) -> Result<()> {
fn check_web(build: &mut Build) -> Result<()> {
let dprint_files = inputs![glob![
"**/*.{ts,mjs,js,md,json,toml,svelte,scss}",
"target/**"
"{target,ts/.svelte-kit,node_modules}/**"
]];
build.add_action(
"check:format:dprint",

@ -1 +1 @@
Subproject commit c74c15b7f82c0f184910e5b6f695b635e6d81faf
Subproject commit e3af3c983241448a239871ca573c9dd2fa5e8619

View file

@ -334,7 +334,7 @@ deck-config-updating-cards = Updating cards: { $current_cards_count }/{ $total_c
deck-config-invalid-weights = Parameters must be either left blank to use the defaults, or must be 17 comma-separated numbers.
deck-config-not-enough-history = Insufficient review history to perform this operation.
deck-config-unable-to-determine-desired-retention =
Unable to determine an optimal retention.
Unable to determine a minimum recommended retention.
deck-config-must-have-400-reviews =
{ $count ->
[one] Only { $count } review was found.
@ -343,21 +343,21 @@ deck-config-must-have-400-reviews =
# Numbers that control how aggressively the FSRS algorithm schedules cards
deck-config-weights = FSRS parameters
deck-config-compute-optimal-weights = Optimize FSRS parameters
deck-config-compute-optimal-retention = Compute optimal retention
deck-config-compute-minimum-recommended-retention = Minimum recommended retention
deck-config-optimize-button = Optimize
deck-config-compute-button = Compute
deck-config-ignore-before = Ignore reviews before
deck-config-optimize-all-tip = You can optimize all presets at once by using the dropdown button next to "Save".
deck-config-evaluate-button = Evaluate
deck-config-desired-retention = Desired retention
deck-config-historical-retention = Historical Retention
deck-config-historical-retention = Historical retention
deck-config-smaller-is-better = Smaller numbers indicate a better fit to your review history.
deck-config-steps-too-large-for-fsrs = When FSRS is enabled, steps of 1 day or more are not recommended.
deck-config-get-params = Get Params
deck-config-fsrs-on-all-clients =
Please ensure all of your Anki clients are Anki(Mobile) 23.10+ or AnkiDroid 2.17+. FSRS will
not work correctly if one of your clients is older.
deck-config-predicted-optimal-retention = Predicted optimal retention: { $num }
deck-config-predicted-minimum-recommended-retention = Minimum recommended retention: { $num }
deck-config-complete = { $num }% complete.
deck-config-iterations = Iteration: { $count }...
deck-config-reschedule-cards-on-change = Reschedule cards on change
@ -473,3 +473,6 @@ deck-config-compute-optimal-retention-tooltip =
if it significantly differs from 0.9, it's a sign that the time you've allocated each day is either too low
or too high for the amount of cards you're trying to learn. This number can be useful as a reference, but it
is not recommended to copy it into the desired retention field.
deck-config-compute-optimal-retention = Compute minimum recommended retention
deck-config-predicted-optimal-retention = Minimum recommended retention: { $num }

View file

@ -174,6 +174,6 @@ scheduling-set-due-date-done =
}
scheduling-forgot-cards =
{ $cards ->
[one] Forgot { $cards } card.
*[other] Forgot { $cards } cards.
[one] Reset { $cards } card.
*[other] Reset { $cards } cards.
}

@ -1 +1 @@
Subproject commit 06ad12df7a2c8400cf64e9c7b986e9ee722e5b38
Subproject commit 45155310c3302cbbbe645dec52ca196894422463

View file

@ -6,7 +6,12 @@ from __future__ import annotations
import logging
import sys
import pip_system_certs.wrapt_requests
try:
import pip_system_certs.wrapt_requests
except ModuleNotFoundError:
print(
"Python module pip_system_certs is not installed. System certificate store and custom SSL certificates may not work. See: https://github.com/ankitects/anki/issues/3016"
)
if sys.version_info[0] < 3 or sys.version_info[1] < 9:
raise Exception("Anki requires Python 3.9+")

View file

@ -191,6 +191,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
"Akash Reddy",
"Marko Sisovic",
"Lucas Scharenbroch",
"Antoine Q.",
)
)

View file

@ -170,7 +170,7 @@ def confirm_full_download(
) -> None:
# confirmation step required, as some users customize their notetypes
# in an empty collection, then want to upload them
if not askUser(tr.sync_confirm_empty_download()):
if not askUser(tr.sync_confirm_empty_download(), parent=mw):
return on_done()
else:
mw.closeAllWindows(lambda: full_download(mw, server_usn, on_done))
@ -182,7 +182,7 @@ def confirm_full_upload(
# confirmation step required, as some users have reported an upload
# happening despite having their AnkiWeb collection not being empty
# (not reproducible - maybe a compiler bug?)
if not askUser(tr.sync_confirm_empty_upload()):
if not askUser(tr.sync_confirm_empty_upload(), parent=mw):
return on_done()
else:
mw.closeAllWindows(lambda: full_upload(mw, server_usn, on_done))

View file

@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import os
import platform
import subprocess
import sys
@ -10,7 +11,7 @@ out_dylib, *src_files = sys.argv[1:]
out_dir = Path(out_dylib).parent.resolve()
src_dir = Path(src_files[0]).parent.resolve()
if platform.machine() == "arm64":
if platform.machine() == "arm64" and not os.environ.get("MAC_X86"):
target = "arm64-apple-macos11"
else:
target = "x86_64-apple-macos10.14"

View file

@ -359,9 +359,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{#if optimalRetention}
{estimatedRetention(optimalRetention)}
{#if parseFloat(optimalRetention.toFixed(2)) > $config.desiredRetention}
{#if optimalRetention - $config.desiredRetention >= 0.01}
<Warning
warning="Your desired retention is below optimal. Increasing it is recommended."
warning={tr.deckConfigDesiredRetentionBelowOptimal()}
className="alert-warning"
/>
{/if}

View file

@ -470,6 +470,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
font-size: 16px !important;
}
:global(.top-tool-icon-button:active) {
background: var(--highlight-bg) !important;
}
.dropdown-content {
display: none;
position: absolute;
@ -479,7 +483,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
.show {
display: flex;
display: table;
}
::-webkit-scrollbar {

View file

@ -28,7 +28,9 @@ export function extractShapesFromClozedField(
group.push(buildShape(shape.shape, props));
}
}
if (group.length > 1) {
if (occlusion.ordinal === 0) {
output.push(...group);
} else if (group.length > 1) {
output.push(group);
} else {
output.push(group[0]);

View file

@ -20,23 +20,71 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {
const shapes = baseShapesFromFabric();
let clozes = "";
let index = 0;
shapes.forEach((shapeOrShapes) => {
// shapes with width or height less than 5 are not valid
if (shapeOrShapes === null) {
return;
}
// if shape is Rect and fill is transparent, skip it
if (shapeOrShapes instanceof Rectangle && shapeOrShapes.fill === "transparent") {
return;
}
clozes += shapeOrShapesToCloze(shapeOrShapes, index, occludeInactive);
if (!(shapeOrShapes instanceof Text)) {
index++;
let noteCount = 0;
// take out all ordinal values from shapes
const ordinalList = shapes.map((shape) => {
if (Array.isArray(shape)) {
return shape[0].ordinal;
} else {
return shape.ordinal;
}
});
return { clozes, noteCount: index };
const filterOrdinalList: number[] = ordinalList.flatMap(v => typeof v === "number" ? [v] : []);
const maxOrdinal = Math.max(...filterOrdinalList, 0);
const missingOrdinals: number[] = [];
for (let i = 1; i <= maxOrdinal; i++) {
if (!ordinalList.includes(i)) {
missingOrdinals.push(i);
}
}
let nextOrdinal = maxOrdinal + 1;
shapes.map((shapeOrShapes) => {
if (shapeOrShapes === null) {
return;
}
// Maintain existing ordinal in editing mode
let ordinal: number | undefined;
if (Array.isArray(shapeOrShapes)) {
ordinal = shapeOrShapes[0].ordinal;
} else {
ordinal = shapeOrShapes.ordinal;
}
if (ordinal === undefined) {
// if ordinal is undefined, assign a missing ordinal if available
if (shapeOrShapes instanceof Text) {
ordinal = 0;
} else if (missingOrdinals.length > 0) {
ordinal = missingOrdinals.shift() as number;
} else {
ordinal = nextOrdinal;
nextOrdinal++;
}
if (Array.isArray(shapeOrShapes)) {
shapeOrShapes.forEach((shape) => (shape.ordinal = ordinal));
} else {
shapeOrShapes.ordinal = ordinal;
}
}
clozes += shapeOrShapesToCloze(
shapeOrShapes,
ordinal,
occludeInactive,
);
if (!(shapeOrShapes instanceof Text)) {
noteCount++;
}
});
return { clozes, noteCount };
}
/** Gather all Fabric shapes, and convert them into BaseShapes or
@ -51,6 +99,7 @@ export function baseShapesFromFabric(): ShapeOrShapes[] {
: null;
const objects = canvas.getObjects() as FabricObject[];
const boundingBox = getBoundingBox();
// filter transparent rectangles
return objects
.map((object) => {
// If the object is in the active selection containing multiple objects,
@ -58,7 +107,9 @@ export function baseShapesFromFabric(): ShapeOrShapes[] {
const parent = selectionContainingMultipleObjects?.contains(object)
? selectionContainingMultipleObjects
: undefined;
if (object.width < 5 || object.height < 5) {
// shapes with width or height less than 5 are not valid
// if shape is Rect and fill is transparent, skip it
if (object.width! < 5 || object.height! < 5 || object.fill == "transparent") {
return null;
}
return fabricObjectToBaseShapeOrShapes(
@ -132,7 +183,7 @@ function fabricObjectToBaseShapeOrShapes(
{{c1::image-occlusion:rect:top=.1:left=.23:width=.4:height=.5}} */
function shapeOrShapesToCloze(
shapeOrShapes: ShapeOrShapes,
index: number,
ordinal: number,
occludeInactive: boolean,
): string {
let text = "";
@ -144,7 +195,7 @@ function shapeOrShapesToCloze(
let type: string;
if (Array.isArray(shapeOrShapes)) {
return shapeOrShapes
.map((shape) => shapeOrShapesToCloze(shape, index, occludeInactive))
.map((shape) => shapeOrShapesToCloze(shape, ordinal, occludeInactive))
.join("");
} else if (shapeOrShapes instanceof Rectangle) {
type = "rect";
@ -165,16 +216,6 @@ function shapeOrShapesToCloze(
addKeyValue("oi", "1");
}
// Maintain existing ordinal in editing mode
let ordinal = shapeOrShapes.ordinal;
if (ordinal === undefined) {
if (type === "text") {
ordinal = 0;
} else {
ordinal = index + 1;
}
shapeOrShapes.ordinal = ordinal;
}
text = `{{c${ordinal}::image-occlusion:${type}${text}}}<br>`;
return text;

View file

@ -11,7 +11,7 @@ export const addShape = (
boundingBox: fabric.Rect,
shape: Shape,
): void => {
const fabricShape = shape.toFabric(boundingBox);
const fabricShape = shape.toFabric(boundingBox.getBoundingRect(true));
addBorder(fabricShape);
if (fabricShape.type === "i-text") {
enableUniformScaling(canvas, fabricShape);
@ -26,7 +26,7 @@ export const addShapeGroup = (
): void => {
const group = new fabric.Group();
shapes.map((shape) => {
const fabricShape = shape.toFabric(boundingBox);
const fabricShape = shape.toFabric(boundingBox.getBoundingRect(true));
addBorder(fabricShape);
group.addWithUpdate(fabricShape);
});

View file

@ -64,12 +64,18 @@ export const groupShapes = (canvas: fabric.Canvas): void => {
const activeObject = canvas.getActiveObject();
const items = activeObject.getObjects();
let minOrdinal: number | undefined = Math.min(...items.map((item) => item.ordinal));
minOrdinal = Number.isNaN(minOrdinal) ? undefined : minOrdinal;
items.forEach((item) => {
item.set({ opacity: 1 });
item.set({ opacity: 1, ordinal: minOrdinal });
});
activeObject.toGroup().set({
opacity: get(opacityStateStore) ? 0.4 : 1,
});
redraw(canvas);
};
@ -85,13 +91,16 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => {
const items = group.getObjects();
group._restoreObjectsState();
group.destroyed = true;
canvas.remove(group);
items.forEach((item) => {
item.set({ opacity: get(opacityStateStore) ? 0.4 : 1 });
item.set({
opacity: get(opacityStateStore) ? 0.4 : 1,
ordinal: undefined,
});
canvas.add(item);
});
canvas.remove(group);
redraw(canvas);
};
@ -282,9 +291,13 @@ export const makeShapeRemainInCanvas = (canvas: fabric.Canvas, boundingBox: fabr
export const selectAllShapes = (canvas: fabric.Canvas) => {
canvas.discardActiveObject();
const sel = new fabric.ActiveSelection(canvas.getObjects(), {
canvas: canvas,
});
// filter out the transparent bounding box from the selection
const sel = new fabric.ActiveSelection(
canvas.getObjects().filter((obj) => obj.fill !== "transparent"),
{
canvas: canvas,
},
);
canvas.setActiveObject(sel);
redraw(canvas);
};

View file

@ -2,7 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import * as tr from "@tslib/ftl";
import type fabric from "fabric";
import fabric from "fabric";
import { writable } from "svelte/store";
import { mdiRedo, mdiUndo } from "../icons";
@ -87,6 +87,12 @@ class UndoStack {
emitChangeSignal();
this.locked = false;
});
// make bounding box unselectable
this.canvas?.forEachObject((obj) => {
if (obj instanceof fabric.Rect && obj.fill === "transparent") {
obj.selectable = false;
}
});
}
onObjectAdded(id: string): void {