From 39c3a8e104ed840c91099699a9a681c1760b1819 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 30 Mar 2022 13:49:07 +1000 Subject: [PATCH 1/8] Add Daniel to about screen As always, anyone who has contributed and is missing, please let me know, or send through a PR. --- qt/aqt/about.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/about.py b/qt/aqt/about.py index aad20436a..116408b1c 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -224,6 +224,7 @@ def show(mw: aqt.AnkiQt) -> QDialog: "Matthias Metelka", "Sergio Quintero", "Nicholas Flint", + "Daniel Vieira Memoria10X", ) ) From f0dc6e103f6a6493ff96219b10906ff5018bf20d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 30 Mar 2022 14:52:16 +1000 Subject: [PATCH 2/8] Fix "repeat cloze" shortcut regression Closes #1748 again. --- ts/editor/editor-toolbar/ClozeButton.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/editor/editor-toolbar/ClozeButton.svelte b/ts/editor/editor-toolbar/ClozeButton.svelte index eb6fa8579..2041b60d2 100644 --- a/ts/editor/editor-toolbar/ClozeButton.svelte +++ b/ts/editor/editor-toolbar/ClozeButton.svelte @@ -8,6 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import IconButton from "../../components/IconButton.svelte"; import Shortcut from "../../components/Shortcut.svelte"; import * as tr from "../../lib/ftl"; + import { isApplePlatform } from "../../lib/platform"; import { getPlatformString } from "../../lib/shortcuts"; import { wrapInternal } from "../../lib/wrap"; import { context as noteEditorContext } from "../NoteEditor.svelte"; @@ -17,6 +18,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const { focusedInput, fields } = noteEditorContext.get(); + // Workaround for Cmd+Option+Shift+C not working on macOS. The keyup approach works + // on Linux as well, but fails on Windows. + const event = isApplePlatform() ? "keyup" : "keydown"; + const clozePattern = /\{\{c(\d+)::/gu; function getCurrentHighestCloze(increment: boolean): number { let highest = 0; @@ -65,6 +70,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html onCloze(event.detail.originalEvent)} /> From 7f737b60c6f6445690f4a612e0a237420daa0cc1 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Thu, 31 Mar 2022 03:17:13 +0200 Subject: [PATCH 3/8] Fix infinite update loop in editor with invalid input HTML (#1761) * Use async function in PlainTextInput * Clean up PlainTextInput * Refactor logic from {Rich,Plain}TextInput into own files * Remove prohibited tags on content.subscribe which also parses the html --- ts/editor/DecoratedElements.svelte | 10 +- ts/editor/MathjaxElement.svelte | 5 +- .../editor-toolbar/TemplateButtons.svelte | 4 +- .../mathjax-overlay/MathjaxHandle.svelte | 4 +- .../plain-text-input/PlainTextInput.svelte | 86 ++++--- .../plain-text-input/remove-prohibited.ts | 11 +- ts/editor/plain-text-input/transform.ts | 12 + .../rich-text-input/RichTextInput.svelte | 225 +++++------------- .../rich-text-input/normalizing-node-store.ts | 25 ++ .../rich-text-input/rich-text-resolve.ts | 43 ++++ ts/editor/rich-text-input/transform.ts | 60 +++++ ts/lib/parsing.ts | 12 + 12 files changed, 267 insertions(+), 230 deletions(-) create mode 100644 ts/editor/plain-text-input/transform.ts create mode 100644 ts/editor/rich-text-input/normalizing-node-store.ts create mode 100644 ts/editor/rich-text-input/rich-text-resolve.ts create mode 100644 ts/editor/rich-text-input/transform.ts create mode 100644 ts/lib/parsing.ts diff --git a/ts/editor/DecoratedElements.svelte b/ts/editor/DecoratedElements.svelte index 150de2585..5c28a6edc 100644 --- a/ts/editor/DecoratedElements.svelte +++ b/ts/editor/DecoratedElements.svelte @@ -4,18 +4,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> - - diff --git a/ts/editor/MathjaxElement.svelte b/ts/editor/MathjaxElement.svelte index 5dbf2441e..212f66989 100644 --- a/ts/editor/MathjaxElement.svelte +++ b/ts/editor/MathjaxElement.svelte @@ -2,11 +2,10 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> - diff --git a/ts/editor/rich-text-input/normalizing-node-store.ts b/ts/editor/rich-text-input/normalizing-node-store.ts new file mode 100644 index 000000000..d75076006 --- /dev/null +++ b/ts/editor/rich-text-input/normalizing-node-store.ts @@ -0,0 +1,25 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import type { DecoratedElement } from "../../editable/decorated"; +import type { NodeStore } from "../../sveltelib/node-store"; +import { nodeStore } from "../../sveltelib/node-store"; +import { decoratedElements } from "../DecoratedElements.svelte"; + +function normalizeFragment(fragment: DocumentFragment): void { + fragment.normalize(); + + for (const decorated of decoratedElements) { + for (const element of fragment.querySelectorAll( + decorated.tagName, + ) as NodeListOf) { + element.undecorate(); + } + } +} + +function getStore(): NodeStore { + return nodeStore(undefined, normalizeFragment); +} + +export default getStore; diff --git a/ts/editor/rich-text-input/rich-text-resolve.ts b/ts/editor/rich-text-input/rich-text-resolve.ts new file mode 100644 index 000000000..21fa109e0 --- /dev/null +++ b/ts/editor/rich-text-input/rich-text-resolve.ts @@ -0,0 +1,43 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import { bridgeCommand } from "../../lib/bridgecommand"; +import { on } from "../../lib/events"; +import { promiseWithResolver } from "../../lib/promise"; + +function bridgeCopyPasteCommands(input: HTMLElement): { destroy(): void } { + function onPaste(event: Event): void { + event.preventDefault(); + bridgeCommand("paste"); + } + + function onCutOrCopy(): void { + bridgeCommand("cutOrCopy"); + } + + const removePaste = on(input, "paste", onPaste); + const removeCopy = on(input, "copy", onCutOrCopy); + const removeCut = on(input, "cut", onCutOrCopy); + + return { + destroy() { + removePaste(); + removeCopy(); + removeCut(); + }, + }; +} + +function useRichTextResolve(): [Promise, (input: HTMLElement) => void] { + const [promise, resolve] = promiseWithResolver(); + + function richTextResolve(input: HTMLElement): { destroy(): void } { + const destroy = bridgeCopyPasteCommands(input); + resolve(input); + return destroy; + } + + return [promise, richTextResolve]; +} + +export default useRichTextResolve; diff --git a/ts/editor/rich-text-input/transform.ts b/ts/editor/rich-text-input/transform.ts new file mode 100644 index 000000000..185992834 --- /dev/null +++ b/ts/editor/rich-text-input/transform.ts @@ -0,0 +1,60 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import { + fragmentToString, + nodeContainsInlineContent, + nodeIsElement, +} from "../../lib/dom"; +import { createDummyDoc } from "../../lib/parsing"; +import { decoratedElements } from "../DecoratedElements.svelte"; + +function adjustInputHTML(html: string): string { + for (const component of decoratedElements) { + html = component.toUndecorated(html); + } + + return html; +} + +function adjustInputFragment(fragment: DocumentFragment): void { + if (nodeContainsInlineContent(fragment)) { + fragment.appendChild(document.createElement("br")); + } +} + +export function storedToFragment(html: string): DocumentFragment { + /* We need .createContextualFragment so that customElements are initialized */ + const fragment = document + .createRange() + .createContextualFragment(createDummyDoc(adjustInputHTML(html))); + + adjustInputFragment(fragment); + return fragment; +} + +function adjustOutputFragment(fragment: DocumentFragment): void { + if ( + fragment.hasChildNodes() && + nodeIsElement(fragment.lastChild!) && + nodeContainsInlineContent(fragment) && + fragment.lastChild!.tagName === "BR" + ) { + fragment.lastChild!.remove(); + } +} + +function adjustOutputHTML(html: string): string { + for (const component of decoratedElements) { + html = component.toStored(html); + } + + return html; +} + +export function fragmentToStored(fragment: DocumentFragment): string { + const clone = document.importNode(fragment, true); + adjustOutputFragment(clone); + + return adjustOutputHTML(fragmentToString(clone)); +} diff --git a/ts/lib/parsing.ts b/ts/lib/parsing.ts new file mode 100644 index 000000000..83ec6eeb9 --- /dev/null +++ b/ts/lib/parsing.ts @@ -0,0 +1,12 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +/** + * Parsing with or without this dummy structure changes the output + * for both `DOMParser.parseAsString` and range.createContextualFragment`. + * Parsing without means that comments or meaningless html elements are dropped, + * which we want to avoid. + */ +export function createDummyDoc(html: string): string { + return `${html}`; +} From 5b1fcccf33de35cd17aab27b21974a32aaacb634 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Thu, 31 Mar 2022 05:30:00 +0200 Subject: [PATCH 4/8] Add extra button group for cloze commands (#1756) * First attempt at adding a directory for icons under //ts * Fix image import * Fix import order * Add cloze button group * Fix issue with toolbar.toolbar dynamically slottable * Change tooltip for repeating cloze deletion * Fix repeat cloze button not working on macOS (dae) --- ftl/core/editing.ftl | 1 + qt/aqt/editor.py | 2 +- ts/BUILD.bazel | 8 ++ ts/editor/BUILD.bazel | 1 + ts/editor/editor-toolbar/BlockButtons.svelte | 117 +++++++++--------- ...ClozeButton.svelte => ClozeButtons.svelte} | 60 ++++++--- ts/editor/editor-toolbar/EditorToolbar.svelte | 5 + .../editor-toolbar/TemplateButtons.svelte | 5 - ts/editor/editor-toolbar/icons.ts | 3 +- ts/icons/BUILD.bazel | 9 ++ ts/icons/contain-plus.svg | 1 + ts/tsconfig.json | 3 +- 12 files changed, 130 insertions(+), 85 deletions(-) rename ts/editor/editor-toolbar/{ClozeButton.svelte => ClozeButtons.svelte} (58%) create mode 100644 ts/icons/BUILD.bazel create mode 100644 ts/icons/contain-plus.svg diff --git a/ftl/core/editing.ftl b/ftl/core/editing.ftl index 585ec5f0f..a49fe3db9 100644 --- a/ftl/core/editing.ftl +++ b/ftl/core/editing.ftl @@ -9,6 +9,7 @@ editing-cards = Cards editing-center = Center editing-change-color = Change color editing-cloze-deletion = Cloze deletion +editing-cloze-deletion-repeat = Repeat cloze deletion editing-couldnt-record-audio-have-you-installed = Couldn't record audio. Have you installed 'lame'? editing-customize-card-templates = Customize Card Templates editing-customize-fields = Customize Fields diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 68c1cc299..ad6094009 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -1387,7 +1387,7 @@ def set_cloze_button(editor: Editor) -> None: action = "show" if editor.note.note_type()["type"] == MODEL_CLOZE else "hide" editor.web.eval( 'require("anki/ui").loaded.then(() =>' - f'require("anki/NoteEditor").instances[0].toolbar.templateButtons.{action}("cloze")' + f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("cloze")' "); " ) diff --git a/ts/BUILD.bazel b/ts/BUILD.bazel index e2679ea7f..9fb25671a 100644 --- a/ts/BUILD.bazel +++ b/ts/BUILD.bazel @@ -35,3 +35,11 @@ alias( name = "node", actual = "@nodejs//:node", ) + +filegroup( + name = "ts", + srcs = [ + "//ts/icons", + ], + visibility = ["//ts:__subpackages__"], +) diff --git a/ts/editor/BUILD.bazel b/ts/editor/BUILD.bazel index 389b7b61b..ca17ca28c 100644 --- a/ts/editor/BUILD.bazel +++ b/ts/editor/BUILD.bazel @@ -47,6 +47,7 @@ _esbuild_deps = [ "//sass:button_mixins_lib", "@npm//@mdi", "@npm//bootstrap-icons", + "//ts/icons", "@npm//protobufjs", ] diff --git a/ts/editor/editor-toolbar/BlockButtons.svelte b/ts/editor/editor-toolbar/BlockButtons.svelte index 1f0e93845..6bf7ba478 100644 --- a/ts/editor/editor-toolbar/BlockButtons.svelte +++ b/ts/editor/editor-toolbar/BlockButtons.svelte @@ -12,7 +12,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } from "../../components/ButtonGroupItem.svelte"; import DynamicallySlottable from "../../components/DynamicallySlottable.svelte"; import IconButton from "../../components/IconButton.svelte"; - import Item from "../../components/Item.svelte"; import Shortcut from "../../components/Shortcut.svelte"; import WithDropdown from "../../components/WithDropdown.svelte"; import { getListItem } from "../../lib/dom"; @@ -92,73 +91,69 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - - {@html justifyLeftIcon} + + {@html justifyLeftIcon} - {@html justifyCenterIcon} + {@html justifyCenterIcon} - {@html justifyRightIcon} + {@html justifyRightIcon} - {@html justifyFullIcon} - - + {@html justifyFullIcon} + - - - - {@html outdentIcon} - + + + {@html outdentIcon} + - + - - {@html indentIcon} - + + {@html indentIcon} + - - - + + diff --git a/ts/editor/editor-toolbar/ClozeButton.svelte b/ts/editor/editor-toolbar/ClozeButtons.svelte similarity index 58% rename from ts/editor/editor-toolbar/ClozeButton.svelte rename to ts/editor/editor-toolbar/ClozeButtons.svelte index 2041b60d2..28bf8fa3a 100644 --- a/ts/editor/editor-toolbar/ClozeButton.svelte +++ b/ts/editor/editor-toolbar/ClozeButtons.svelte @@ -5,6 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - {@html ellipseIcon} - + + + {@html incrementClozeIcon} + - onCloze(event.detail.originalEvent)} -/> + + + + {@html clozeIcon} + + + + diff --git a/ts/editor/editor-toolbar/EditorToolbar.svelte b/ts/editor/editor-toolbar/EditorToolbar.svelte index 455b3f7d9..cd00895bc 100644 --- a/ts/editor/editor-toolbar/EditorToolbar.svelte +++ b/ts/editor/editor-toolbar/EditorToolbar.svelte @@ -56,6 +56,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import Item from "../../components/Item.svelte"; import StickyContainer from "../../components/StickyContainer.svelte"; import BlockButtons from "./BlockButtons.svelte"; + import ClozeButtons from "./ClozeButtons.svelte"; import InlineButtons from "./InlineButtons.svelte"; import NotetypeButtons from "./NotetypeButtons.svelte"; import TemplateButtons from "./TemplateButtons.svelte"; @@ -105,6 +106,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + + + + diff --git a/ts/editor/editor-toolbar/TemplateButtons.svelte b/ts/editor/editor-toolbar/TemplateButtons.svelte index a6e311ea3..528b9368c 100644 --- a/ts/editor/editor-toolbar/TemplateButtons.svelte +++ b/ts/editor/editor-toolbar/TemplateButtons.svelte @@ -20,7 +20,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { context } from "../NoteEditor.svelte"; import { setFormat } from "../old-editor-adapter"; import { editingInputIsRichText } from "../rich-text-input"; - import ClozeButton from "./ClozeButton.svelte"; import { micIcon, paperclipIcon } from "./icons"; import LatexButton from "./LatexButton.svelte"; @@ -116,10 +115,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html /> - - - - diff --git a/ts/editor/editor-toolbar/icons.ts b/ts/editor/editor-toolbar/icons.ts index 70647187b..0bc292d07 100644 --- a/ts/editor/editor-toolbar/icons.ts +++ b/ts/editor/editor-toolbar/icons.ts @@ -24,7 +24,8 @@ export { default as underlineIcon } from "bootstrap-icons/icons/type-underline.s export const arrowIcon = ''; -export { default as ellipseIcon } from "@mdi/svg/svg/contain.svg"; +export { default as incrementClozeIcon } from "../../icons/contain-plus.svg"; +export { default as clozeIcon } from "@mdi/svg/svg/contain.svg"; export { default as functionIcon } from "@mdi/svg/svg/function-variant.svg"; export { default as paperclipIcon } from "@mdi/svg/svg/paperclip.svg"; export { default as micIcon } from "bootstrap-icons/icons/mic.svg"; diff --git a/ts/icons/BUILD.bazel b/ts/icons/BUILD.bazel new file mode 100644 index 000000000..d5a816504 --- /dev/null +++ b/ts/icons/BUILD.bazel @@ -0,0 +1,9 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") + +js_library( + name = "icons", + srcs = glob([ + "*.svg", + ]), + visibility = ["//visibility:public"], +) diff --git a/ts/icons/contain-plus.svg b/ts/icons/contain-plus.svg new file mode 100644 index 000000000..c815e09c7 --- /dev/null +++ b/ts/icons/contain-plus.svg @@ -0,0 +1 @@ + diff --git a/ts/tsconfig.json b/ts/tsconfig.json index 42a371ec7..0e9fbf09d 100644 --- a/ts/tsconfig.json +++ b/ts/tsconfig.json @@ -12,7 +12,8 @@ { "path": "reviewer" }, { "path": "lib" }, { "path": "domlib" }, - { "path": "sveltelib" } + { "path": "sveltelib" }, + { "path": "icons" } ], "compilerOptions": { "declaration": true, From 9d64afc7bc92e1acee3af394ff997f91d3fe27bf Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 31 Mar 2022 10:35:15 +1000 Subject: [PATCH 5/8] Handle gaps in media in colpkg imports Our old Python code was also skipping numbers when it encountered a directory, leading to a colpkg that couldn't be imported with our new code. --- proto/anki/import_export.proto | 5 ++ .../import_export/package/colpkg/export.rs | 1 + .../import_export/package/colpkg/import.rs | 46 +++++++++---------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/proto/anki/import_export.proto b/proto/anki/import_export.proto index 94711c31a..ea8bfe8ad 100644 --- a/proto/anki/import_export.proto +++ b/proto/anki/import_export.proto @@ -46,6 +46,11 @@ message MediaEntries { string name = 1; uint32 size = 2; bytes sha1 = 3; + + /// Legacy media maps may include gaps in the media list, so the original + /// file index is recorded when importing from a HashMap. This field is not + /// set when exporting. + optional uint32 legacy_zip_filename = 255; } repeated MediaEntry entries = 1; diff --git a/rslib/src/import_export/package/colpkg/export.rs b/rslib/src/import_export/package/colpkg/export.rs index e2247bc83..6dc5de69f 100644 --- a/rslib/src/import_export/package/colpkg/export.rs +++ b/rslib/src/import_export/package/colpkg/export.rs @@ -278,6 +278,7 @@ impl MediaEntry { name: name.into(), size: size.try_into().unwrap_or_default(), sha1: sha1.into(), + legacy_zip_filename: None, } } } diff --git a/rslib/src/import_export/package/colpkg/import.rs b/rslib/src/import_export/package/colpkg/import.rs index 7664637a4..bd6a6fe00 100644 --- a/rslib/src/import_export/package/colpkg/import.rs +++ b/rslib/src/import_export/package/colpkg/import.rs @@ -102,16 +102,22 @@ fn restore_media( let media_entries = extract_media_entries(meta, archive)?; std::fs::create_dir_all(media_folder)?; - for (archive_file_name, entry) in media_entries.iter().enumerate() { - if archive_file_name % 10 == 0 { - progress_fn(ImportProgress::Media(archive_file_name))?; + for (entry_idx, entry) in media_entries.iter().enumerate() { + if entry_idx % 10 == 0 { + progress_fn(ImportProgress::Media(entry_idx))?; } - if let Ok(mut zip_file) = archive.by_name(&archive_file_name.to_string()) { + let zip_filename = entry + .legacy_zip_filename + .map(|n| n as usize) + .unwrap_or(entry_idx) + .to_string(); + + if let Ok(mut zip_file) = archive.by_name(&zip_filename) { maybe_restore_media_file(meta, media_folder, entry, &mut zip_file)?; } else { return Err(AnkiError::invalid_input(&format!( - "{archive_file_name} missing from archive" + "{zip_filename} missing from archive" ))); } } @@ -203,27 +209,17 @@ fn extract_media_entries(meta: &Meta, archive: &mut ZipArchive) -> Result< } if meta.media_list_is_hashmap() { let map: HashMap<&str, String> = serde_json::from_slice(&buf)?; - let mut entries: Vec<(usize, String)> = map - .into_iter() - .map(|(k, v)| (k.parse().unwrap_or_default(), v)) - .collect(); - entries.sort_unstable(); - // any gaps in the file numbers would lead to media being imported under the wrong name - if entries - .iter() - .enumerate() - .any(|(idx1, (idx2, _))| idx1 != *idx2) - { - return Err(AnkiError::ImportError(ImportError::Corrupt)); - } - Ok(entries - .into_iter() - .map(|(_str_idx, name)| MediaEntry { - name, - size: 0, - sha1: vec![], + map.into_iter() + .map(|(idx_str, name)| { + let idx: u32 = idx_str.parse()?; + Ok(MediaEntry { + name, + size: 0, + sha1: vec![], + legacy_zip_filename: Some(idx), + }) }) - .collect()) + .collect() } else { let entries: MediaEntries = Message::decode(&*buf)?; Ok(entries.entries) From dc36a08f3ab6ea316eb0ef19bd0242acc7929564 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 31 Mar 2022 06:31:13 +0200 Subject: [PATCH 6/8] Also restore/keep position of new cards (#1760) * Also restore/keep position of new cards * Refactor Card::last_position() --- rslib/src/scheduler/new.rs | 70 ++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/rslib/src/scheduler/new.rs b/rslib/src/scheduler/new.rs index b40d06e8a..05a0d6999 100644 --- a/rslib/src/scheduler/new.rs +++ b/rslib/src/scheduler/new.rs @@ -18,9 +18,31 @@ use crate::{ }; impl Card { - fn schedule_as_new(&mut self, position: u32, reset_counts: bool) { + pub(crate) fn original_or_current_due(&self) -> i32 { + self.is_filtered() + .then(|| self.original_due) + .unwrap_or(self.due) + } + + pub(crate) fn last_position(&self) -> Option { + if self.ctype == CardType::New { + Some(self.original_or_current_due() as u32) + } else { + self.original_position + } + } + + /// True if the provided position has been used. + /// (Always true, if restore_position is false.) + pub(crate) fn schedule_as_new( + &mut self, + position: u32, + reset_counts: bool, + restore_position: bool, + ) -> bool { + let last_position = restore_position.then(|| self.last_position()).flatten(); self.remove_from_filtered_deck_before_reschedule(); - self.due = position as i32; + self.due = last_position.unwrap_or(position) as i32; self.ctype = CardType::New; self.queue = CardQueue::New; self.interval = 0; @@ -30,6 +52,8 @@ impl Card { self.reps = 0; self.lapses = 0; } + + last_position.is_none() } /// If the card is new, change its position, and return true. @@ -135,10 +159,7 @@ impl Collection { let cards = col.storage.all_searched_cards_in_search_order()?; for mut card in cards { let original = card.clone(); - if restore_position && card.original_position.is_some() { - card.schedule_as_new(card.original_position.unwrap(), reset_counts); - } else { - card.schedule_as_new(position, reset_counts); + if card.schedule_as_new(position, reset_counts, restore_position) { position += 1; } if log { @@ -301,6 +322,43 @@ mod test { } unreachable!("not random"); } + + #[test] + fn last_position() { + // new card + let mut card = Card::new(NoteId(0), 0, DeckId(1), 42); + assert_eq!(card.last_position(), Some(42)); + // in filtered deck + card.original_deck_id.0 = 1; + card.deck_id.0 = 2; + card.original_due = 42; + card.due = 123456789; + card.queue = CardQueue::Review; + assert_eq!(card.last_position(), Some(42)); + + // graduated card + let mut card = Card::new(NoteId(0), 0, DeckId(1), 42); + card.queue = CardQueue::Review; + card.ctype = CardType::Review; + card.due = 123456789; + // only recent clients remember the original position + assert_eq!(card.last_position(), None); + card.original_position = Some(42); + assert_eq!(card.last_position(), Some(42)); + } + + #[test] + fn scheduling_as_new() { + let mut card = Card::new(NoteId(0), 0, DeckId(1), 42); + card.reps = 4; + card.lapses = 2; + // keep counts and position + card.schedule_as_new(1, false, true); + assert_eq!((card.due, card.reps, card.lapses), (42, 4, 2)); + // complete reset + card.schedule_as_new(1, true, false); + assert_eq!((card.due, card.reps, card.lapses), (1, 0, 0)); + } } impl From for NewCardDueOrder { From 6df63f86438201055c363fa78a77a9b40a069436 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 31 Mar 2022 14:42:58 +1000 Subject: [PATCH 7/8] Tweak cloze deletion labels --- ftl/core/editing.ftl | 4 ++-- ts/editor/editor-toolbar/ClozeButtons.svelte | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ftl/core/editing.ftl b/ftl/core/editing.ftl index a49fe3db9..f91dface4 100644 --- a/ftl/core/editing.ftl +++ b/ftl/core/editing.ftl @@ -8,8 +8,8 @@ editing-bold-text = Bold text editing-cards = Cards editing-center = Center editing-change-color = Change color -editing-cloze-deletion = Cloze deletion -editing-cloze-deletion-repeat = Repeat cloze deletion +editing-cloze-deletion = Cloze deletion (new card) +editing-cloze-deletion-repeat = Cloze deletion (same card) editing-couldnt-record-audio-have-you-installed = Couldn't record audio. Have you installed 'lame'? editing-customize-card-templates = Customize Card Templates editing-customize-fields = Customize Fields diff --git a/ts/editor/editor-toolbar/ClozeButtons.svelte b/ts/editor/editor-toolbar/ClozeButtons.svelte index 28bf8fa3a..5dd391d2b 100644 --- a/ts/editor/editor-toolbar/ClozeButtons.svelte +++ b/ts/editor/editor-toolbar/ClozeButtons.svelte @@ -72,9 +72,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html Date: Thu, 31 Mar 2022 14:48:08 +1000 Subject: [PATCH 8/8] Update translations --- repos.bzl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repos.bzl b/repos.bzl index 39dfeaa6e..e0877a60f 100644 --- a/repos.bzl +++ b/repos.bzl @@ -115,12 +115,12 @@ def register_repos(): ################ core_i18n_repo = "anki-core-i18n" - core_i18n_commit = "bd995b3d74f37975554ebd03d3add4ea82bf663f" - core_i18n_zip_csum = "ace985f858958321d5919731981bce2b9356ea3e8fb43b0232a1dc6f55673f3d" + core_i18n_commit = "7d1954863a721e09d974c609b55fa78f0e178b0f" + core_i18n_zip_csum = "14cce5d0ecd2c00ce839d735ab7fe979439080ad9c510cc6fc2e63c97a9c745c" qtftl_i18n_repo = "anki-desktop-ftl" - qtftl_i18n_commit = "5045d3604a20b0ae8ce14be2d3597d72c03ccad8" - qtftl_i18n_zip_csum = "45058ea33cb0e5d142cae8d4e926f5eb3dab4d207e7af0baeafda2b92f765806" + qtftl_i18n_commit = "31baaae83fad4be3d8977d6053ef3032bdb90481" + qtftl_i18n_zip_csum = "96e42e0278affb2586e677b52b466e6ca8bb4f3fd080a561683c48b483202fa2" i18n_build_content = """ filegroup(