From e92aaa4478b7158ada94dfbbd2d0c5621c227ab0 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 19 Aug 2024 13:22:19 +0700 Subject: [PATCH 1/7] 'card type, then order gathered' https://forums.ankiweb.net/t/rename-card-type-to-card-type-then-order-gathered/48046 --- ftl/core/deck-config.ftl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index ca89dbd8211..113afb34b96 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -160,7 +160,7 @@ deck-config-new-gather-priority-random-notes = Random notes deck-config-new-gather-priority-random-cards = Random cards deck-config-new-card-sort-order = New card sort order deck-config-new-card-sort-order-tooltip-2 = - `Card type`: Displays cards in order of card type number. If you have sibling burying + `Card type, then order gathered`: Displays cards in order of card type number. If you have sibling burying disabled, this will ensure all front→back cards are seen before any back→front cards. This is useful to have all cards of the same note shown in the same session, but not too close to one another. @@ -180,7 +180,7 @@ deck-config-new-card-sort-order-tooltip-2 = deck-config-sort-order-card-template-then-random = Card type, then random deck-config-sort-order-random-note-then-template = Random note, then card type deck-config-sort-order-random = Random -deck-config-sort-order-template-then-gather = Card type +deck-config-sort-order-template-then-gather = Card type, then order gathered deck-config-sort-order-gather = Order gathered deck-config-new-review-priority = New/review order deck-config-new-review-priority-tooltip = When to show new cards in relation to review cards. From 8ed9f49bdc11492a6d499f3d64b9dc125102e11d Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 22 Aug 2024 16:34:19 +0800 Subject: [PATCH 2/7] Feat/FSRS Simulator (#3257) * test using existed cards * plot new and review * convert learning cards & use line chart * allow draw multiple simulations in the same chart * support hide simulation * convert x axis to Date * convert y from second to minute * support clear last simulation * remove unused import * rename * add hover/tooltip * fallback to default parameters * update default value and maximum of deckSize * add "processing..." * fix mistake --- qt/aqt/mediasrv.py | 1 + rslib/src/scheduler/fsrs/simulator.rs | 80 +++++--- ts/routes/deck-options/FsrsOptions.svelte | 178 ++++++++++++++++- ts/routes/graphs/simulator.ts | 220 ++++++++++++++++++++++ 4 files changed, 457 insertions(+), 22 deletions(-) create mode 100644 ts/routes/graphs/simulator.ts diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index a613a4795d0..c866ae423f2 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -625,6 +625,7 @@ def handle_on_main() -> None: "set_wants_abort", "evaluate_weights", "get_optimal_retention_parameters", + "simulate_fsrs_review", ] diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index 2396f14acee..b41d98d76eb 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -5,8 +5,10 @@ use anki_proto::scheduler::SimulateFsrsReviewRequest; use anki_proto::scheduler::SimulateFsrsReviewResponse; use fsrs::simulate; use fsrs::SimulatorConfig; +use fsrs::DEFAULT_PARAMETERS; use itertools::Itertools; +use crate::card::CardQueue; use crate::prelude::*; use crate::search::SortMode; @@ -22,9 +24,15 @@ impl Collection { .get_revlog_entries_for_searched_cards_in_card_order()?; let cards = guard.col.storage.all_searched_cards()?; drop(guard); + let days_elapsed = self.timing_today().unwrap().days_elapsed as i32; + let converted_cards = cards + .into_iter() + .filter(|c| c.queue != CardQueue::Suspended && c.queue != CardQueue::PreviewRepeat) + .filter_map(|c| Card::convert(c, days_elapsed, req.days_to_simulate)) + .collect_vec(); let p = self.get_optimal_retention_parameters(revlogs)?; let config = SimulatorConfig { - deck_size: req.deck_size as usize, + deck_size: req.deck_size as usize + converted_cards.len(), learn_span: req.days_to_simulate as usize, max_cost_perday: f32::MAX, max_ivl: req.max_interval as f32, @@ -40,7 +48,19 @@ impl Collection { learn_limit: req.new_limit as usize, review_limit: req.review_limit as usize, }; - let days_elapsed = self.timing_today().unwrap().days_elapsed as i32; + let parameters = if req.weights.is_empty() { + DEFAULT_PARAMETERS.to_vec() + } else if req.weights.len() != 19 { + if req.weights.len() == 17 { + let mut parameters = req.weights.to_vec(); + parameters.extend_from_slice(&[0.0, 0.0]); + parameters + } else { + return Err(AnkiError::FsrsWeightsInvalid); + } + } else { + req.weights.to_vec() + }; let ( accumulated_knowledge_acquisition, daily_review_count, @@ -48,15 +68,10 @@ impl Collection { daily_time_cost, ) = simulate( &config, - &req.weights, + ¶meters, req.desired_retention, None, - Some( - cards - .into_iter() - .filter_map(|c| Card::convert(c, days_elapsed)) - .collect_vec(), - ), + Some(converted_cards), ); Ok(SimulateFsrsReviewResponse { accumulated_knowledge_acquisition: accumulated_knowledge_acquisition.to_vec(), @@ -68,19 +83,42 @@ impl Collection { } impl Card { - fn convert(card: Card, days_elapsed: i32) -> Option { + fn convert(card: Card, days_elapsed: i32, day_to_simulate: u32) -> Option { match card.memory_state { - Some(state) => { - let due = card.original_or_current_due(); - let relative_due = due - days_elapsed; - Some(fsrs::Card { - difficulty: state.difficulty, - stability: state.stability, - last_date: (relative_due - card.interval as i32) as f32, - due: relative_due as f32, - }) - } - None => None, + Some(state) => match card.queue { + CardQueue::DayLearn | CardQueue::Review => { + let due = card.original_or_current_due(); + let relative_due = due - days_elapsed; + Some(fsrs::Card { + difficulty: state.difficulty, + stability: state.stability, + last_date: (relative_due - card.interval as i32) as f32, + due: relative_due as f32, + }) + } + CardQueue::New => Some(fsrs::Card { + difficulty: 1e-10, + stability: 1e-10, + last_date: 0.0, + due: day_to_simulate as f32, + }), + CardQueue::Learn | CardQueue::SchedBuried | CardQueue::UserBuried => { + Some(fsrs::Card { + difficulty: state.difficulty, + stability: state.stability, + last_date: 0.0, + due: 0.0, + }) + } + CardQueue::PreviewRepeat => None, + CardQueue::Suspended => None, + }, + None => Some(fsrs::Card { + difficulty: 1e-10, + stability: 1e-10, + last_date: 0.0, + due: day_to_simulate as f32, + }), } } } diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index e836e1dc8b4..569dee9ddc9 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -7,10 +7,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ComputeRetentionProgress, type ComputeWeightsProgress, } from "@generated/anki/collection_pb"; - import { ComputeOptimalRetentionRequest } from "@generated/anki/scheduler_pb"; + import { + ComputeOptimalRetentionRequest, + SimulateFsrsReviewRequest, + type SimulateFsrsReviewResponse, + } from "@generated/anki/scheduler_pb"; import { computeFsrsWeights, computeOptimalRetention, + simulateFsrsReview, evaluateWeights, setWantsAbort, } from "@generated/backend"; @@ -28,6 +33,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import Warning from "./Warning.svelte"; import WeightsInputRow from "./WeightsInputRow.svelte"; import WeightsSearchRow from "./WeightsSearchRow.svelte"; + import { renderSimulationChart, type Point } from "../graphs/simulator"; + import Graph from "../graphs/Graph.svelte"; + import HoverColumns from "../graphs/HoverColumns.svelte"; + import CumulativeOverlay from "../graphs/CumulativeOverlay.svelte"; + import AxisTicks from "../graphs/AxisTicks.svelte"; + import NoDataOverlay from "../graphs/NoDataOverlay.svelte"; + import TableData from "../graphs/TableData.svelte"; + import { defaultGraphBounds, type TableDatum } from "../graphs/graph-helpers"; export let state: DeckOptionsState; export let openHelpModal: (String) => void; @@ -68,6 +81,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html optimalRetentionRequest.daysToSimulate = 3650; } + const simulateFsrsRequest = new SimulateFsrsReviewRequest({ + weights: $config.fsrsWeights, + desiredRetention: $config.desiredRetention, + deckSize: 0, + daysToSimulate: 365, + newLimit: $config.newPerDay, + reviewLimit: $config.reviewsPerDay, + maxInterval: $config.maximumReviewInterval, + search: `preset:"${state.getCurrentName()}" -is:suspended`, + }); + function getRetentionWarning(retention: number): string { const decay = -0.5; const factor = 0.9 ** (1 / decay) - 1; @@ -256,6 +280,69 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } return tr.deckConfigPredictedOptimalRetention({ num: retention.toFixed(2) }); } + + let tableData: TableDatum[] = [] as any; + const bounds = defaultGraphBounds(); + let svg = null as HTMLElement | SVGElement | null; + const title = tr.statisticsReviewsTitle(); + let simulationNumber = 0; + + let points: Point[] = []; + + function movingAverage(y: number[], windowSize: number): number[] { + const result: number[] = []; + for (let i = 0; i < y.length; i++) { + let sum = 0; + let count = 0; + for (let j = Math.max(0, i - windowSize + 1); j <= i; j++) { + sum += y[j]; + count++; + } + result.push(sum / count); + } + return result; + } + + $: simulateProgressString = ""; + + async function simulateFsrs(): Promise { + let resp: SimulateFsrsReviewResponse | undefined; + simulationNumber += 1; + try { + await runWithBackendProgress( + async () => { + simulateFsrsRequest.weights = $config.fsrsWeights; + simulateFsrsRequest.desiredRetention = $config.desiredRetention; + simulateFsrsRequest.search = `preset:"${state.getCurrentName()}" -is:suspended`; + simulateProgressString = "processing..."; + resp = await simulateFsrsReview(simulateFsrsRequest); + }, + () => {}, + ); + } finally { + if (resp) { + simulateProgressString = ""; + const dailyTimeCost = movingAverage( + resp.dailyTimeCost, + Math.round(simulateFsrsRequest.daysToSimulate / 50), + ); + points = points.concat( + dailyTimeCost.map((v, i) => ({ + x: i, + y: v, + label: simulationNumber, + })), + ); + tableData = renderSimulationChart(svg as SVGElement, bounds, points); + } + } + } + + function clearSimulation(): void { + points = points.filter((p) => p.label !== simulationNumber); + simulationNumber = Math.max(0, simulationNumber - 1); + tableData = renderSimulationChart(svg as SVGElement, bounds, points); + } +
+
+ FSRS simulator (experimental) + + + openHelpModal("simulateFsrsReview")}> + Days to simulate + + + + + openHelpModal("simulateFsrsReview")}> + Additional new cards to simulate + + + + + openHelpModal("simulateFsrsReview")}> + New cards/day + + + + + openHelpModal("simulateFsrsReview")}> + Maximum reviews/day + + + + + openHelpModal("simulateFsrsReview")}> + Maximum interval + + + + + + +
{simulateProgressString}
+ + + + + + + + + + + +
+
+ diff --git a/ts/routes/graphs/simulator.ts b/ts/routes/graphs/simulator.ts new file mode 100644 index 00000000000..52c7917c633 --- /dev/null +++ b/ts/routes/graphs/simulator.ts @@ -0,0 +1,220 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import { localizedNumber } from "@tslib/i18n"; +import { + axisBottom, + axisLeft, + bisector, + line, + max, + pointer, + rollup, + scaleLinear, + scaleTime, + schemeCategory10, + select, + timeFormat, +} from "d3"; + +import type { GraphBounds, TableDatum } from "./graph-helpers"; +import { setDataAvailable } from "./graph-helpers"; +import { hideTooltip, showTooltip } from "./tooltip"; + +export interface Point { + x: number; + y: number; + label: number; +} + +export function renderSimulationChart( + svgElem: SVGElement, + bounds: GraphBounds, + data: Point[], +): TableDatum[] { + const svg = select(svgElem); + svg.selectAll(".lines").remove(); + svg.selectAll(".hover-columns").remove(); + svg.selectAll(".focus-line").remove(); + svg.selectAll(".legend").remove(); + if (data.length == 0) { + setDataAvailable(svg, false); + return []; + } + const trans = svg.transition().duration(600) as any; + + // Prepare data + const today = new Date(); + const convertedData = data.map(d => ({ + ...d, + date: new Date(today.getTime() + d.x * 24 * 60 * 60 * 1000), + yMinutes: d.y / 60, + })); + const xMin = today; + const xMax = max(convertedData, d => d.date); + + const x = scaleTime() + .domain([xMin, xMax!]) + .range([bounds.marginLeft, bounds.width - bounds.marginRight]); + const formatDate = timeFormat("%Y-%m-%d"); + + svg.select(".x-ticks") + .call((selection) => + selection.transition(trans).call( + axisBottom(x) + .ticks(7) + .tickFormat((d: any) => formatDate(d)) + .tickSizeOuter(0), + ) + ) + .attr("direction", "ltr"); + // y scale + + const yTickFormat = (n: number): string => { + if (Math.round(n) != n) { + return ""; + } else { + return localizedNumber(n); + } + }; + + const yMax = max(convertedData, d => d.yMinutes)!; + const y = scaleLinear() + .range([bounds.height - bounds.marginBottom, bounds.marginTop]) + .domain([0, yMax]) + .nice(); + svg.select(".y-ticks") + .call((selection) => + selection.transition(trans).call( + axisLeft(y) + .ticks(bounds.height / 50) + .tickSizeOuter(0) + .tickFormat(yTickFormat as any), + ) + ) + .attr("direction", "ltr"); + + svg.select(".y-ticks") + .append("text") + .attr("class", "y-axis-title") + .attr("transform", "rotate(-90)") + .attr("y", 0 - bounds.marginLeft) + .attr("x", 0 - (bounds.height / 2)) + .attr("dy", "1em") + .attr("fill", "currentColor") + .style("text-anchor", "middle") + .text("Review Time per day (minutes)"); + + // x lines + const points = convertedData.map((d) => [x(d.date), y(d.yMinutes), d.label]); + const groups = rollup(points, v => Object.assign(v, { z: v[0][2] }), d => d[2]); + + const color = schemeCategory10; + + svg.append("g") + .attr("class", "lines") + .attr("fill", "none") + .attr("stroke-width", 1.5) + .attr("stroke-linejoin", "round") + .attr("stroke-linecap", "round") + .selectAll("path") + .data(Array.from(groups.entries())) + .join("path") + .style("mix-blend-mode", "multiply") + .attr("stroke", (d, i) => color[i % color.length]) + .attr("d", d => line()(d[1].map(p => [p[0], p[1]]))) + .attr("data-group", d => d[0]); + + const focusLine = svg.append("line") + .attr("class", "focus-line") + .attr("y1", bounds.marginTop) + .attr("y2", bounds.height - bounds.marginBottom) + .attr("stroke", "black") + .attr("stroke-width", 1) + .style("opacity", 0); + + const LongestGroupData = Array.from(groups.values()).reduce((a, b) => a.length > b.length ? a : b); + const barWidth = bounds.width / LongestGroupData.length; + + // hover/tooltip + svg.append("g") + .attr("class", "hover-columns") + .selectAll("rect") + .data(LongestGroupData) + .join("rect") + .attr("x", d => d[0] - barWidth / 2) + .attr("y", bounds.marginTop) + .attr("width", barWidth) + .attr("height", bounds.height - bounds.marginTop - bounds.marginBottom) + .attr("fill", "transparent") + .on("mousemove", mousemove) + .on("mouseout", hideTooltip); + + function mousemove(event: MouseEvent, d: any): void { + pointer(event, document.body); + const date = x.invert(d[0]); + + const groupData: { [key: string]: number } = {}; + + groups.forEach((groupPoints, key) => { + const bisect = bisector((d: number[]) => x.invert(d[0])).left; + const index = bisect(groupPoints, date); + const dataPoint = groupPoints[index - 1] || groupPoints[index]; + + if (dataPoint) { + groupData[key] = y.invert(dataPoint[1]); + } + }); + + focusLine.attr("x1", d[0]).attr("x2", d[0]).style("opacity", 1); + + let tooltipContent = `Date: ${timeFormat("%Y-%m-%d")(date)}
`; + for (const [key, value] of Object.entries(groupData)) { + tooltipContent += `Simulation ${key}: ${value.toFixed(2)} minutes
`; + } + + showTooltip(tooltipContent, event.pageX, event.pageY); + } + + const legend = svg.append("g") + .attr("class", "legend") + .attr("font-family", "sans-serif") + .attr("font-size", 10) + .attr("text-anchor", "start") + .selectAll("g") + .data(Array.from(groups.keys())) + .join("g") + .attr("transform", (d, i) => `translate(0,${i * 20})`) + .attr("cursor", "pointer") + .on("click", (event, d) => toggleGroup(event, d)); + + legend.append("rect") + .attr("x", bounds.width - bounds.marginRight + 10) + .attr("width", 19) + .attr("height", 19) + .attr("fill", (d, i) => color[i % color.length]); + + legend.append("text") + .attr("x", bounds.width - bounds.marginRight + 34) + .attr("y", 9.5) + .attr("dy", "0.32em") + .text(d => `Simulation ${d}`); + + const toggleGroup = (event: MouseEvent, d: number) => { + const group = d; + const path = svg.select(`path[data-group="${group}"]`); + const hidden = path.classed("hidden"); + const target = event.currentTarget as HTMLElement; + + path.classed("hidden", !hidden); + path.style("display", () => hidden ? null : "none"); + + select(target).select("rect") + .style("opacity", hidden ? 1 : 0.5); + }; + + setDataAvailable(svg, true); + + const tableData: TableDatum[] = []; + + return tableData; +} From 922958b0aeee017b659778f1f706badcfd12184f Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 22 Aug 2024 17:02:50 +0800 Subject: [PATCH 3/7] Update to FSRS-rs v1.1.5 (#3369) --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- cargo/licenses.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 718f7c1adff..cfa6a21066e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1901,9 +1901,9 @@ dependencies = [ [[package]] name = "fsrs" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "285d9b275f7d5a276f17006e9d92ea67fa9991187ae88760fa96705fba1f97aa" +checksum = "f5b4e9d166a106007cc88e2ec7c01b107cf4999bbef71d74d32142cdf5277802" dependencies = [ "burn", "itertools 0.12.1", diff --git a/Cargo.toml b/Cargo.toml index b119e8c0a49..081dea700b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git" rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] -version = "1.1.4" +version = "1.1.5" # git = "https://github.com/open-spaced-repetition/fsrs-rs.git" # rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941" # path = "../open-spaced-repetition/fsrs-rs" diff --git a/cargo/licenses.json b/cargo/licenses.json index 01e615b762b..88c33dda4e1 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -1252,7 +1252,7 @@ }, { "name": "fsrs", - "version": "1.1.4", + "version": "1.1.5", "authors": "Open Spaced Repetition", "repository": "https://github.com/open-spaced-repetition/fsrs-rs", "license": "BSD-3-Clause", From a6d5c949970627f2b4dcea8a02fea3a497e0440f Mon Sep 17 00:00:00 2001 From: David Culley <6276049+davidculley@users.noreply.github.com> Date: Thu, 22 Aug 2024 11:03:44 +0200 Subject: [PATCH 4/7] python: add missing type annotations for None values (#3364) * refactor: explicitly add NoneType to type hints If variable can be `None`, don't be implicit. Be explicit. --- pylib/anki/collection.py | 6 +++--- pylib/anki/config.py | 2 +- pylib/anki/decks.py | 2 +- pylib/anki/httpclient.py | 2 +- pylib/anki/models.py | 2 +- pylib/anki/template.py | 2 +- qt/aqt/addons.py | 4 +++- qt/aqt/browser/sidebar/item.py | 2 +- qt/aqt/browser/sidebar/tree.py | 2 +- qt/aqt/browser/table/table.py | 4 +++- qt/aqt/editor.py | 4 ++-- qt/aqt/import_export/exporting.py | 2 +- qt/aqt/importing.py | 2 +- qt/aqt/main.py | 4 ++-- qt/aqt/progress.py | 2 +- qt/aqt/reviewer.py | 8 ++++---- qt/aqt/switch.py | 4 +++- qt/aqt/utils.py | 14 +++++++------- 18 files changed, 37 insertions(+), 31 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index a501de728d9..b3bad9922a9 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -897,7 +897,7 @@ def set_browser_note_columns(self, columns: list[str]) -> None: # Config ########################################################################## - def get_config(self, key: str, default: Any = None) -> Any: + def get_config(self, key: str, default: Any | None = None) -> Any: try: return self.conf.get_immutable(key) except KeyError: @@ -939,7 +939,7 @@ def set_config_string( return self._backend.set_config_string(key=key, value=value, undoable=undoable) def get_aux_notetype_config( - self, id: NotetypeId, key: str, default: Any = None + self, id: NotetypeId, key: str, default: Any | None = None ) -> Any: key = self._backend.get_aux_notetype_config_key(id=id, key=key) return self.get_config(key, default=default) @@ -951,7 +951,7 @@ def set_aux_notetype_config( return self.set_config(key, value, undoable=undoable) def get_aux_template_config( - self, id: NotetypeId, card_ordinal: int, key: str, default: Any = None + self, id: NotetypeId, card_ordinal: int, key: str, default: Any | None = None ) -> Any: key = self._backend.get_aux_template_config_key( notetype_id=id, card_ordinal=card_ordinal, key=key diff --git a/pylib/anki/config.py b/pylib/anki/config.py index 834af500442..775557c848f 100644 --- a/pylib/anki/config.py +++ b/pylib/anki/config.py @@ -76,7 +76,7 @@ def __getitem__(self, key: str) -> Any: def __setitem__(self, key: str, value: Any) -> None: self.set(key, value) - def get(self, key: str, default: Any = None) -> Any: + def get(self, key: str, default: Any | None = None) -> Any: try: return self[key] except KeyError: diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index e61721dd832..c53275ae300 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -85,7 +85,7 @@ def __init__(self, col: anki.collection.Collection) -> None: self.col = col.weakref() self.decks = DecksDictProxy(col) - def save(self, deck_or_config: DeckDict | DeckConfigDict = None) -> None: + def save(self, deck_or_config: DeckDict | DeckConfigDict | None = None) -> None: "Can be called with either a deck or a deck configuration." if not deck_or_config: print("col.decks.save() should be passed the changed deck") diff --git a/pylib/anki/httpclient.py b/pylib/anki/httpclient.py index 186f9062302..d0a9fdd659f 100644 --- a/pylib/anki/httpclient.py +++ b/pylib/anki/httpclient.py @@ -57,7 +57,7 @@ def post(self, url: str, data: bytes, headers: dict[str, str] | None) -> Respons verify=self.verify, ) # pytype: disable=wrong-arg-types - def get(self, url: str, headers: dict[str, str] = None) -> Response: + def get(self, url: str, headers: dict[str, str] | None = None) -> Response: if headers is None: headers = {} headers["User-Agent"] = self._agent_name() diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 4dfdb0211f8..fb08fd6f350 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -563,7 +563,7 @@ def update( self._mutate_after_write(notetype) # @deprecated(replaced_by=update_dict) - def save(self, notetype: NotetypeDict = None, **legacy_kwargs: bool) -> None: + def save(self, notetype: NotetypeDict | None = None, **legacy_kwargs: bool) -> None: "Save changes made to provided note type." if not notetype: print_deprecation_warning( diff --git a/pylib/anki/template.py b/pylib/anki/template.py index deda8c81951..2a145c9e555 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -147,7 +147,7 @@ def __init__( card: anki.cards.Card, note: anki.notes.Note, browser: bool = False, - notetype: NotetypeDict = None, + notetype: NotetypeDict | None = None, template: dict | None = None, fill_empty: bool = False, ) -> None: diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 00d68c09850..b4f3868e757 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -409,7 +409,9 @@ def allAddonConflicts(self) -> dict[str, list[str]]: all_conflicts[other_dir].append(addon.dir_name) return all_conflicts - def _disableConflicting(self, module: str, conflicts: list[str] = None) -> set[str]: + def _disableConflicting( + self, module: str, conflicts: list[str] | None = None + ) -> set[str]: if not self.isEnabled(module): # disabled add-ons should not trigger conflict handling return set() diff --git a/qt/aqt/browser/sidebar/item.py b/qt/aqt/browser/sidebar/item.py index 576d0b45517..8da2f3df60a 100644 --- a/qt/aqt/browser/sidebar/item.py +++ b/qt/aqt/browser/sidebar/item.py @@ -62,7 +62,7 @@ def __init__( name: str, icon: str | ColoredIcon, search_node: SearchNode | None = None, - on_expanded: Callable[[bool], None] = None, + on_expanded: Callable[[bool], None] | None = None, expanded: bool = False, item_type: SidebarItemType = SidebarItemType.CUSTOM, id: int = 0, diff --git a/qt/aqt/browser/sidebar/tree.py b/qt/aqt/browser/sidebar/tree.py index 6189e925c80..89600f2313a 100644 --- a/qt/aqt/browser/sidebar/tree.py +++ b/qt/aqt/browser/sidebar/tree.py @@ -159,7 +159,7 @@ def refresh_if_needed(self) -> None: self.refresh() self._refresh_needed = False - def refresh(self, new_current: SidebarItem = None) -> None: + def refresh(self, new_current: SidebarItem | None = None) -> None: "Refresh list. No-op if sidebar is not visible." if not self.isVisible(): return diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index 345386acbe7..54631ebfa5f 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -600,7 +600,9 @@ def _scroll_to_column(self, column: int) -> None: self._view.verticalScrollBar().setValue(vertical) def _move_current( - self, direction: QAbstractItemView.CursorAction, index: QModelIndex = None + self, + direction: QAbstractItemView.CursorAction, + index: QModelIndex | None = None, ) -> None: if not self.has_current(): return diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 6f0a994d7df..74cc6e55baa 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -233,9 +233,9 @@ def addButton( func: Callable[[Editor], None], tip: str = "", label: str = "", - id: str = None, + id: str | None = None, toggleable: bool = False, - keys: str = None, + keys: str | None = None, disables: bool = True, rightside: bool = True, ) -> str: diff --git a/qt/aqt/import_export/exporting.py b/qt/aqt/import_export/exporting.py index a33395f98f0..ad7fc4ef6a2 100644 --- a/qt/aqt/import_export/exporting.py +++ b/qt/aqt/import_export/exporting.py @@ -137,7 +137,7 @@ def get_out_path(self) -> str | None: return path def options(self, out_path: str) -> ExportOptions: - limit: ExportLimit = None + limit: ExportLimit | None = None if self.nids: limit = NoteIdsLimit(self.nids) elif current_deck_id := self.current_deck_id(): diff --git a/qt/aqt/importing.py b/qt/aqt/importing.py index b00d0b69ba5..ffc173a8b9a 100644 --- a/qt/aqt/importing.py +++ b/qt/aqt/importing.py @@ -120,7 +120,7 @@ def setupOptions(self) -> None: ) self.deck = aqt.deckchooser.DeckChooser(self.mw, self.frm.deckArea, label=False) - def modelChanged(self, unused: Any = None) -> None: + def modelChanged(self, unused: Any | None = None) -> None: self.importer.model = self.mw.col.models.current() self.importer.initMapping() self.showMapping() diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 9508cc10d39..a27ec8abdca 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -862,8 +862,8 @@ def reset(self, unused_arg: bool = False) -> None: def requireReset( self, modal: bool = False, - reason: Any = None, - context: Any = None, + reason: Any | None = None, + context: Any | None = None, ) -> None: traceback.print_stack(file=sys.stdout) print("requireReset() is obsolete; please use CollectionOp()") diff --git a/qt/aqt/progress.py b/qt/aqt/progress.py index dd8fb26e1b7..84d23a22eef 100644 --- a/qt/aqt/progress.py +++ b/qt/aqt/progress.py @@ -42,7 +42,7 @@ def timer( repeat: bool, requiresCollection: bool = True, *, - parent: QObject = None, + parent: QObject | None = None, ) -> QTimer: """Create and start a standard Anki timer. For an alternative see `single_shot()`. diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 680d323b12c..0913d049de5 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -152,8 +152,8 @@ def __init__(self, mw: AnkiQt) -> None: self.previous_card: Card | None = None self._answeredIds: list[CardId] = [] self._recordedAudio: str | None = None - self.typeCorrect: str = None # web init happens before this is set - self.state: Literal["question", "answer", "transition", None] = None + self.typeCorrect: str | None = None # web init happens before this is set + self.state: Literal["question", "answer", "transition"] | None = None self._refresh_needed: RefreshNeeded | None = None self._v3: V3CardInfo | None = None self._state_mutation_key = str(random.randint(0, 2**64 - 1)) @@ -162,7 +162,7 @@ def __init__(self, mw: AnkiQt) -> None: self._previous_card_info = PreviousReviewerCardInfo(self.mw) self._states_mutated = True self._state_mutation_js = None - self._reps: int = None + self._reps: int | None = None self._show_question_timer: QTimer | None = None self._show_answer_timer: QTimer | None = None self.auto_advance_enabled = False @@ -369,7 +369,7 @@ def _mungeQA(self, buf: str) -> str: def _showQuestion(self) -> None: self._reps += 1 self.state = "question" - self.typedAnswer: str = None + self.typedAnswer: str | None = None c = self.card # grab the question and play audio q = c.question() diff --git a/qt/aqt/switch.py b/qt/aqt/switch.py index 15f3c08902c..fb3c2da6c51 100644 --- a/qt/aqt/switch.py +++ b/qt/aqt/switch.py @@ -1,5 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + from typing import cast from aqt import colors, props @@ -21,7 +23,7 @@ def __init__( right_label: str = "", left_color: dict[str, str] = colors.ACCENT_CARD | {}, right_color: dict[str, str] = colors.ACCENT_NOTE | {}, - parent: QWidget = None, + parent: QWidget | None = None, ) -> None: super().__init__(parent=parent) self.setCheckable(True) diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 98e4cee1091..8d777455522 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -393,8 +393,8 @@ def onFinish() -> None: def askUser( text: str, - parent: QWidget = None, - help: HelpPageArgument = None, + parent: QWidget | None = None, + help: HelpPageArgument | None = None, defaultno: bool = False, msgfunc: Callable | None = None, title: str = "Anki", @@ -426,7 +426,7 @@ def __init__( text: str, buttons: list[str], parent: QWidget | None = None, - help: HelpPageArgument = None, + help: HelpPageArgument | None = None, title: str = "Anki", ): QMessageBox.__init__(self, parent) @@ -459,7 +459,7 @@ def askUserDialog( text: str, buttons: list[str], parent: QWidget | None = None, - help: HelpPageArgument = None, + help: HelpPageArgument | None = None, title: str = "Anki", ) -> ButtonedDialog: if not parent: @@ -473,7 +473,7 @@ def __init__( self, parent: QWidget | None, question: str, - help: HelpPageArgument = None, + help: HelpPageArgument | None = None, edit: QLineEdit | None = None, default: str = "", title: str = "Anki", @@ -525,7 +525,7 @@ def helpRequested(self) -> None: def getText( prompt: str, parent: QWidget | None = None, - help: HelpPageArgument = None, + help: HelpPageArgument | None = None, edit: QLineEdit | None = None, default: str = "", title: str = "Anki", @@ -558,7 +558,7 @@ def getOnlyText(*args: Any, **kwargs: Any) -> str: # fixme: these utilities could be combined into a single base class # unused by Anki, but used by add-ons def chooseList( - prompt: str, choices: list[str], startrow: int = 0, parent: Any = None + prompt: str, choices: list[str], startrow: int = 0, parent: Any | None = None ) -> int: if not parent: parent = aqt.mw.app.activeWindow() From 7ea573b004c976d20d95d706893993c1b6d79f74 Mon Sep 17 00:00:00 2001 From: Jake Probst Date: Thu, 22 Aug 2024 02:53:41 -0700 Subject: [PATCH 5/7] don't ignore buried cards in future due graph (#3368) it does ignore them for the current day but not days in the future --- rslib/src/stats/graphs/future_due.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rslib/src/stats/graphs/future_due.rs b/rslib/src/stats/graphs/future_due.rs index ef7da2ad373..4671766e5ed 100644 --- a/rslib/src/stats/graphs/future_due.rs +++ b/rslib/src/stats/graphs/future_due.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use anki_proto::stats::graphs_response::FutureDue; use super::GraphsContext; +use crate::card::CardQueue; use crate::scheduler::timing::is_unix_epoch_timestamp; impl GraphsContext { @@ -13,7 +14,7 @@ impl GraphsContext { let mut have_backlog = false; let mut due_by_day: HashMap = Default::default(); for c in &self.cards { - if c.queue as i8 <= 0 { + if matches!(c.queue, CardQueue::New | CardQueue::Suspended) { continue; } let due = c.original_or_current_due(); @@ -24,6 +25,10 @@ impl GraphsContext { due - (self.days_elapsed as i32) }; + // still want to filtered out buried cards that are due today + if due_day == 0 && matches!(c.queue, CardQueue::UserBuried | CardQueue::SchedBuried) { + continue; + } have_backlog |= due_day < 0; *due_by_day.entry(due_day).or_default() += 1; } From 83f044491b5d601fe76d433922ee89fea467adb4 Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 22 Aug 2024 13:35:48 +0300 Subject: [PATCH 6/7] Ensure profile name is treated in a case-insensitive manner (#3372) --- qt/aqt/profiles.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 40ee952645d..469908c1b23 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -213,7 +213,8 @@ def load(self, name: str) -> bool: if name == "_global": raise Exception("_global is not a valid name") data = self.db.scalar( - "select cast(data as blob) from profiles where name = ?", name + "select cast(data as blob) from profiles where name = ? collate nocase", + name, ) self.name = name try: @@ -232,22 +233,26 @@ def load(self, name: str) -> bool: return True def save(self) -> None: - sql = "update profiles set data = ? where name = ?" + sql = "update profiles set data = ? where name = ? collate nocase" self.db.execute(sql, self._pickle(self.profile), self.name) self.db.execute(sql, self._pickle(self.meta), "_global") self.db.commit() def create(self, name: str) -> None: prof = profileConf.copy() + if self.db.scalar("select 1 from profiles where name = ? collate nocase", name): + return self.db.execute( - "insert or ignore into profiles values (?, ?)", name, self._pickle(prof) + "insert or ignore into profiles values (?, ?)", + name, + self._pickle(prof), ) self.db.commit() def remove(self, name: str) -> None: path = self.profileFolder(create=False) send_to_trash(Path(path)) - self.db.execute("delete from profiles where name = ?", name) + self.db.execute("delete from profiles where name = ? collate nocase", name) self.db.commit() def trashCollection(self) -> None: @@ -277,7 +282,9 @@ def rename(self, name: str) -> None: return # update name - self.db.execute("update profiles set name = ? where name = ?", name, oldName) + self.db.execute( + "update profiles set name = ? where name = ? collate nocase", name, oldName + ) # rename folder try: os.rename(oldFolder, newFolder) @@ -403,7 +410,7 @@ def recover() -> None: self.db.execute( """ create table if not exists profiles -(name text primary key, data blob not null);""" +(name text primary key collate nocase, data blob not null);""" ) data = self.db.scalar( "select cast(data as blob) from profiles where name = '_global'" @@ -485,7 +492,7 @@ def _onLangSelected(self) -> None: def setLang(self, code: str) -> None: self.meta["defaultLang"] = code - sql = "update profiles set data = ? where name = ?" + sql = "update profiles set data = ? where name = ? collate nocase" self.db.execute(sql, self._pickle(self.meta), "_global") self.db.commit() anki.lang.set_lang(code) From a179da382759b9c7f8265faf37efdcbb47d8ee08 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 22 Aug 2024 18:24:56 +0700 Subject: [PATCH 7/7] Update dprint (#3376) * Update amd64 docker container to Debian 11 This bumps the minimum required glibc to 2.29, which is 2019 Ubuntu/Fedora, and 2021 Debian. Also remove the unused download of ninja * Update to latest dprint Unblocked by the glibc upgrade --- .buildkite/linux/docker/Dockerfile.amd64 | 9 +-- .dprint.json | 15 ++-- Cargo.toml | 28 ++++---- package.json | 2 +- ts/editor/CollapseBadge.svelte | 3 +- ts/lib/components/Col.svelte | 4 +- ts/lib/components/ScrollArea.svelte | 16 +++-- ts/lib/sass/base.scss | 3 +- ts/routes/deck-options/DailyLimits.svelte | 4 +- ts/routes/graphs/GraphsPage.svelte | 2 +- yarn.lock | 87 ++++++++++++++--------- 11 files changed, 99 insertions(+), 74 deletions(-) diff --git a/.buildkite/linux/docker/Dockerfile.amd64 b/.buildkite/linux/docker/Dockerfile.amd64 index 0223bb2b6d8..7c99c39e052 100644 --- a/.buildkite/linux/docker/Dockerfile.amd64 +++ b/.buildkite/linux/docker/Dockerfile.amd64 @@ -1,4 +1,4 @@ -FROM debian:10-slim +FROM debian:11-slim ARG DEBIAN_FRONTEND="noninteractive" @@ -53,13 +53,6 @@ RUN mkdir -p /etc/buildkite-agent/hooks && chown -R user /etc/buildkite-agent COPY buildkite.cfg /etc/buildkite-agent/buildkite-agent.cfg COPY environment /etc/buildkite-agent/hooks/environment -# Available in Debian 11 as ninja-build, but we're building with Debian 10 -RUN curl -LO https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-linux.zip \ - && unzip ninja-linux.zip \ - && chmod +x ninja \ - && mv ninja /usr/bin \ - && rm ninja-linux.zip - RUN mkdir /state/rust && chown user /state/rust USER user diff --git a/.dprint.json b/.dprint.json index 65f451981ee..b73c6db10da 100644 --- a/.dprint.json +++ b/.dprint.json @@ -31,14 +31,15 @@ "target", ".mypy_cache", "extra", - "ts/.svelte-kit" + "ts/.svelte-kit", + "ts/vite.config.ts.timestamp*" ], "plugins": [ - "https://plugins.dprint.dev/typescript-0.85.1.wasm", - "https://plugins.dprint.dev/json-0.17.4.wasm", - "https://plugins.dprint.dev/markdown-0.15.3.wasm", - "https://plugins.dprint.dev/toml-0.5.4.wasm", - "https://plugins.dprint.dev/prettier-0.13.0.json@dc5d12b7c1bf1a4683eff317c2c87350e75a5a3dfcc127f3d5628931bfb534b1", - "https://plugins.dprint.dev/disrupted/css-0.2.2.wasm" + "https://plugins.dprint.dev/typescript-0.91.6.wasm", + "https://plugins.dprint.dev/json-0.19.3.wasm", + "https://plugins.dprint.dev/markdown-0.17.6.wasm", + "https://plugins.dprint.dev/toml-0.6.2.wasm", + "https://plugins.dprint.dev/prettier-0.46.1.json@e5bd083088a8dfc6e5ce2d3c9bee81489b065bd5345ef55b59f5d96627928b7a", + "https://plugins.dprint.dev/disrupted/css-0.2.3.wasm" ] } diff --git a/Cargo.toml b/Cargo.toml index 081dea700b9..8486f1e0377 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,27 @@ [workspace.package] version = "0.0.0" authors = ["Ankitects Pty Ltd and contributors "] +edition = "2021" license = "AGPL-3.0-or-later" rust-version = "1.65" -edition = "2021" [workspace] members = [ + "build/configure", + "build/ninja_gen", + "build/runner", + "ftl", + "pylib/rsbridge", + "qt/bundle/mac", + "qt/bundle/win", "rslib", "rslib/i18n", - "rslib/linkchecker", - "rslib/proto", "rslib/io", + "rslib/linkchecker", "rslib/process", + "rslib/proto", "rslib/sync", - "pylib/rsbridge", - "build/configure", - "build/ninja_gen", - "build/runner", - "ftl", "tools/minilints", - "qt/bundle/win", - "qt/bundle/mac", ] exclude = ["qt/bundle"] resolver = "2" @@ -45,8 +45,8 @@ version = "1.1.5" anki = { path = "rslib" } anki_i18n = { path = "rslib/i18n" } anki_io = { path = "rslib/io" } -anki_proto = { path = "rslib/proto" } anki_process = { path = "rslib/process" } +anki_proto = { path = "rslib/proto" } anki_proto_gen = { path = "rslib/proto_gen" } ninja_gen = { "path" = "build/ninja_gen" } @@ -74,6 +74,9 @@ criterion = { version = "0.5.1" } csv = "1.3.0" data-encoding = "2.6.0" difflib = "0.4.0" +dirs = "5.0.1" +dunce = "1.0.4" +envy = "0.4.2" flate2 = "1.0.30" fluent = "0.16.1" fluent-bundle = "0.15.3" @@ -145,9 +148,6 @@ wiremock = "0.5.22" xz2 = "0.1.7" zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] } zstd = { version = "0.13.2", features = ["zstdmt"] } -envy = "0.4.2" -dirs = "5.0.1" -dunce = "1.0.4" # Apply mild optimizations to our dependencies in dev mode, which among other things # improves sha2 performance by about 21x. Opt 1 chosen due to diff --git a/package.json b/package.json index 93048143600..dcc6b3f481f 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "caniuse-lite": "^1.0.30001431", "cross-env": "^7.0.2", "diff": "^5.0.0", - "dprint": "=0.35.3", + "dprint": "^0.47.2", "esbuild": "^0.18.10", "esbuild-sass-plugin": "^2", "esbuild-svelte": "^0.7.4", diff --git a/ts/editor/CollapseBadge.svelte b/ts/editor/CollapseBadge.svelte index f2b434afc85..d004116e5a9 100644 --- a/ts/editor/CollapseBadge.svelte +++ b/ts/editor/CollapseBadge.svelte @@ -18,7 +18,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html .collapse-badge { display: inline-block; opacity: 0.4; - transition: opacity var(--transition) ease-in-out, + transition: + opacity var(--transition) ease-in-out, transform var(--transition) ease-in; :global(.collapse-label:hover) & { opacity: 1; diff --git a/ts/lib/components/Col.svelte b/ts/lib/components/Col.svelte index 9123c5631bf..539c7000492 100644 --- a/ts/lib/components/Col.svelte +++ b/ts/lib/components/Col.svelte @@ -48,8 +48,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html "md": $calc, "lg": $calc, "xl": $calc, - "xxl": $calc - ) + "xxl": $calc, + ), ) ); diff --git a/ts/lib/components/ScrollArea.svelte b/ts/lib/components/ScrollArea.svelte index 5a2eab16470..5667c8c5d24 100644 --- a/ts/lib/components/ScrollArea.svelte +++ b/ts/lib/components/ScrollArea.svelte @@ -62,10 +62,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - {#if scrollStates.top}
{/if} - {#if scrollStates.bottom}
{/if} - {#if scrollStates.left}
{/if} - {#if scrollStates.right}
{/if} + {#if scrollStates.top} +
+ {/if} + {#if scrollStates.bottom} +
+ {/if} + {#if scrollStates.left} +
+ {/if} + {#if scrollStates.right} +
+ {/if}
diff --git a/ts/lib/sass/base.scss b/ts/lib/sass/base.scss index 90a33b8d39c..c997eb20a22 100644 --- a/ts/lib/sass/base.scss +++ b/ts/lib/sass/base.scss @@ -56,7 +56,8 @@ body { button:not(.btn, .btn-close) { /* override transition for instant hover response */ - transition: color var(--transition) ease-in-out, + transition: + color var(--transition) ease-in-out, box-shadow var(--transition) ease-in-out !important; border-radius: prop(border-radius); @include button.base; diff --git a/ts/routes/deck-options/DailyLimits.svelte b/ts/routes/deck-options/DailyLimits.svelte index ae5412f7172..d3e5ce74cef 100644 --- a/ts/routes/deck-options/DailyLimits.svelte +++ b/ts/routes/deck-options/DailyLimits.svelte @@ -86,7 +86,7 @@ ), new ValueTab( tr.deckConfigTodayOnly(), - $limits.newTodayActive ? $limits.newToday ?? null : null, + $limits.newTodayActive ? ($limits.newToday ?? null) : null, (value) => ($limits.newToday = value ?? undefined), null, $limits.newToday ?? null, @@ -110,7 +110,7 @@ ), new ValueTab( tr.deckConfigTodayOnly(), - $limits.reviewTodayActive ? $limits.reviewToday ?? null : null, + $limits.reviewTodayActive ? ($limits.reviewToday ?? null) : null, (value) => ($limits.reviewToday = value ?? undefined), null, $limits.reviewToday ?? null, diff --git a/ts/routes/graphs/GraphsPage.svelte b/ts/routes/graphs/GraphsPage.svelte index 172681b7a2c..8a20ac28d49 100644 --- a/ts/routes/graphs/GraphsPage.svelte +++ b/ts/routes/graphs/GraphsPage.svelte @@ -18,7 +18,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const search = writable(initialSearch); const days = writable(initialDays); - export let graphs: typeof SvelteComponent[]; + export let graphs: (typeof SvelteComponent)[]; /** See RangeBox */ export let controller: typeof SvelteComponent | null = RangeBox; diff --git a/yarn.lock b/yarn.lock index 4274d0d3cb1..723276d5f23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,6 +32,46 @@ "@typescript/vfs" "^1.4.0" typescript "4.5.2" +"@dprint/darwin-arm64@0.47.2": + version "0.47.2" + resolved "https://registry.yarnpkg.com/@dprint/darwin-arm64/-/darwin-arm64-0.47.2.tgz#dea8bfa146159e565e510266a2f1d7ebd2676090" + integrity sha512-mVPFBJsXxGDKHHCAY8wbqOyS4028g1bN15H9tivCnPAjwaZhkUimZHXWejXADjhGn+Xm2SlakugY9PY/68pH3Q== + +"@dprint/darwin-x64@0.47.2": + version "0.47.2" + resolved "https://registry.yarnpkg.com/@dprint/darwin-x64/-/darwin-x64-0.47.2.tgz#9e03a7ebb1a38ffcd46fbade96e98e8326d4c77c" + integrity sha512-T7wzlc+rBV+6BRRiBjoqoy5Hj4TR2Nv2p2s9+ycyPGs10Kj/JXOWD8dnEHeBgUr2r4qe/ZdcxmsFQ5Hf2n0WuA== + +"@dprint/linux-arm64-glibc@0.47.2": + version "0.47.2" + resolved "https://registry.yarnpkg.com/@dprint/linux-arm64-glibc/-/linux-arm64-glibc-0.47.2.tgz#7d6c70b1c097d8dd8756cb4cdff40afdde4d42f3" + integrity sha512-B0m1vT5LdVtrNOVdkqpLPrSxuCD+l5bTIgRzPaDoIB1ChWQkler9IlX8C+RStpujjPj6SYvwo5vTzjQSvRdQkA== + +"@dprint/linux-arm64-musl@0.47.2": + version "0.47.2" + resolved "https://registry.yarnpkg.com/@dprint/linux-arm64-musl/-/linux-arm64-musl-0.47.2.tgz#76158f3400848d6d00023559c7b1fbf1cf1198c9" + integrity sha512-zID6wZZqpg2/Q2Us+ERQkbhLwlW3p3xaeEr00MPf49bpydmEjMiPuSjWPkNv+slQSIyIsVovOxF4lbNZjsdtvw== + +"@dprint/linux-x64-glibc@0.47.2": + version "0.47.2" + resolved "https://registry.yarnpkg.com/@dprint/linux-x64-glibc/-/linux-x64-glibc-0.47.2.tgz#1c30261e7a085bcbd6d673c7ac4064a4e50c1a02" + integrity sha512-rB3WXMdINnRd33DItIp7mObS7dzHW90ZzeJSsoKJLPp+Z7wXjjb27UUowfqVI4baa/1pd7sdbX54DPohMtfu/A== + +"@dprint/linux-x64-musl@0.47.2": + version "0.47.2" + resolved "https://registry.yarnpkg.com/@dprint/linux-x64-musl/-/linux-x64-musl-0.47.2.tgz#27d50baca893bbaa8d5ff6448621a1d68d5eba1b" + integrity sha512-E0+TNbzYdTXJ/jCVjUctVxkda/faw++aDQLfyWGcmdMJnbM7NZz+W4fUpDXzMPsjy+zTWxXcPK7/q2DZz2gnbg== + +"@dprint/win32-arm64@0.47.2": + version "0.47.2" + resolved "https://registry.yarnpkg.com/@dprint/win32-arm64/-/win32-arm64-0.47.2.tgz#eb7eb2b4acd17d8221ae85c835c80aa1b6b3110c" + integrity sha512-K1EieTCFjfOCmyIhw9zFSduE6qVCNHEveupqZEfbSkVGw5T9MJQ1I9+n7MDb3RIDYEUk0enJ58/w82q8oDKCyA== + +"@dprint/win32-x64@0.47.2": + version "0.47.2" + resolved "https://registry.yarnpkg.com/@dprint/win32-x64/-/win32-x64-0.47.2.tgz#117c4b22de19dd11991851f27424ccc332d2266a" + integrity sha512-LhizWr8VrhHvq4ump8HwOERyFmdLiE8C6A42QSntGXzKdaa2nEOq20x/o56ZIiDcesiV+1TmosMKimPcOZHa+Q== + "@esbuild/aix-ppc64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" @@ -1336,11 +1376,6 @@ buffer-crc32@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== - buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1981,13 +2016,19 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.3" -dprint@=0.35.3: - version "0.35.3" - resolved "https://registry.yarnpkg.com/dprint/-/dprint-0.35.3.tgz#be89ff5e76b46c21d5db84015ee9232e82e9bc14" - integrity sha512-BWFdr2Ury2dtqyjaTI9P5vCpb1yn5laLfO3Yo5egrrDlK2A2mv6CSpe/oK2cuOPESMY1JT2dubMv2dPDiJfMnA== - dependencies: - https-proxy-agent "=5.0.1" - yauzl "=2.10.0" +dprint@^0.47.2: + version "0.47.2" + resolved "https://registry.yarnpkg.com/dprint/-/dprint-0.47.2.tgz#f3aca518324b9948066652c87e4c4a3bc509869d" + integrity sha512-geUcVIIrmLaY+YtuOl4gD7J/QCjsXZa5gUqre9sO6cgH0X/Fa9heBN3l/AWVII6rKPw45ATuCSDWz1pyO+HkPQ== + optionalDependencies: + "@dprint/darwin-arm64" "0.47.2" + "@dprint/darwin-x64" "0.47.2" + "@dprint/linux-arm64-glibc" "0.47.2" + "@dprint/linux-arm64-musl" "0.47.2" + "@dprint/linux-x64-glibc" "0.47.2" + "@dprint/linux-x64-musl" "0.47.2" + "@dprint/win32-arm64" "0.47.2" + "@dprint/win32-x64" "0.47.2" eastasianwidth@^0.2.0: version "0.2.0" @@ -2463,13 +2504,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== - dependencies: - pend "~1.2.0" - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2758,7 +2792,7 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" -https-proxy-agent@=5.0.1, https-proxy-agent@^5.0.0: +https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -3559,11 +3593,6 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== - periscopic@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a" @@ -4634,14 +4663,6 @@ yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yauzl@=2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"