mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Compare commits
12 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ccd9ca1a83 | ||
![]() |
6643b1bc58 | ||
![]() |
b8e7dd060f | ||
![]() |
e72fec18ae | ||
![]() |
ca24eed1ac | ||
![]() |
4daf293d91 | ||
![]() |
1e50172caf | ||
![]() |
88a67b700c | ||
![]() |
5ad68fc3bf | ||
![]() |
ab8b75298f | ||
![]() |
5fa2693677 | ||
![]() |
f638f8c5b7 |
18 changed files with 131 additions and 54 deletions
2
.version
2
.version
|
@ -1 +1 @@
|
|||
24.04
|
||||
24.04.1
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
|
@ -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+")
|
||||
|
|
|
@ -191,6 +191,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
|||
"Akash Reddy",
|
||||
"Marko Sisovic",
|
||||
"Lucas Scharenbroch",
|
||||
"Antoine Q.",
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue