mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00

* Update to latest Node LTS * Add sveltekit * Split tslib into separate @generated and @tslib components SvelteKit's path aliases don't support multiple locations, so our old approach of using @tslib to refer to both ts/lib and out/ts/lib will no longer work. Instead, all generated sources and their includes are placed in a separate out/ts/generated folder, and imported via @generated instead. This also allows us to generate .ts files, instead of needing to output separate .d.ts and .js files. * Switch package.json to module type * Avoid usage of baseUrl Incompatible with SvelteKit * Move sass into ts; use relative links SvelteKit's default sass support doesn't allow overriding loadPaths * jest->vitest, graphs example working with yarn dev * most pages working in dev mode * Some fixes after rebasing * Fix/silence some svelte-check errors * Get image-occlusion working with Fabric types * Post-rebase lock changes * Editor is now checked * SvelteKit build integrated into ninja * Use the new SvelteKit entrypoint for pages like congrats/deck options/etc * Run eslint once for ts/**; fix some tests * Fix a bunch of issues introduced when rebasing over latest main * Run eslint fix * Fix remaining eslint+pylint issues; tests now all pass * Fix some issues with a clean build * Latest bufbuild no longer requires @__PURE__ hack * Add a few missed dependencies * Add yarn.bat to fix Windows build * Fix pages failing to show when ANKI_API_PORT not defined * Fix svelte-check and vitest on Windows * Set node path in ./yarn * Move svelte-kit output to ts/.svelte-kit Sadly, I couldn't figure out a way to store it in out/ if out/ is a symlink, as it breaks module resolution when SvelteKit is run. * Allow HMR inside Anki * Skip SvelteKit build when HMR is defined * Fix some post-rebase issues I should have done a normal merge instead.
206 lines
6 KiB
TypeScript
206 lines
6 KiB
TypeScript
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import * as tr from "@generated/ftl";
|
|
|
|
export const SECOND = 1.0;
|
|
export const MINUTE = 60.0 * SECOND;
|
|
export const HOUR = 60.0 * MINUTE;
|
|
export const DAY = 24.0 * HOUR;
|
|
export const MONTH = 30.0 * DAY;
|
|
export const YEAR = 12.0 * MONTH;
|
|
|
|
export enum TimespanUnit {
|
|
Seconds,
|
|
Minutes,
|
|
Hours,
|
|
Days,
|
|
Months,
|
|
Years,
|
|
}
|
|
|
|
export function unitName(unit: TimespanUnit): string {
|
|
switch (unit) {
|
|
case TimespanUnit.Seconds:
|
|
return "seconds";
|
|
case TimespanUnit.Minutes:
|
|
return "minutes";
|
|
case TimespanUnit.Hours:
|
|
return "hours";
|
|
case TimespanUnit.Days:
|
|
return "days";
|
|
case TimespanUnit.Months:
|
|
return "months";
|
|
case TimespanUnit.Years:
|
|
return "years";
|
|
}
|
|
}
|
|
|
|
export function naturalUnit(secs: number): TimespanUnit {
|
|
secs = Math.abs(secs);
|
|
if (secs < MINUTE) {
|
|
return TimespanUnit.Seconds;
|
|
} else if (secs < HOUR) {
|
|
return TimespanUnit.Minutes;
|
|
} else if (secs < DAY) {
|
|
return TimespanUnit.Hours;
|
|
} else if (secs < MONTH) {
|
|
return TimespanUnit.Days;
|
|
} else if (secs < YEAR) {
|
|
return TimespanUnit.Months;
|
|
} else {
|
|
return TimespanUnit.Years;
|
|
}
|
|
}
|
|
|
|
/** Number of seconds in a given unit. */
|
|
export function unitSeconds(unit: TimespanUnit): number {
|
|
switch (unit) {
|
|
case TimespanUnit.Seconds:
|
|
return SECOND;
|
|
case TimespanUnit.Minutes:
|
|
return MINUTE;
|
|
case TimespanUnit.Hours:
|
|
return HOUR;
|
|
case TimespanUnit.Days:
|
|
return DAY;
|
|
case TimespanUnit.Months:
|
|
return MONTH;
|
|
case TimespanUnit.Years:
|
|
return YEAR;
|
|
}
|
|
}
|
|
|
|
export function unitAmount(unit: TimespanUnit, secs: number): number {
|
|
return secs / unitSeconds(unit);
|
|
}
|
|
|
|
/** Largest unit provided seconds can be divided by without a remainder. */
|
|
export function naturalWholeUnit(secs: number): TimespanUnit {
|
|
let unit = naturalUnit(secs);
|
|
while (unit != TimespanUnit.Seconds) {
|
|
const amount = Math.round(unitAmount(unit, secs));
|
|
if (Math.abs(secs - amount * unitSeconds(unit)) < Number.EPSILON) {
|
|
return unit;
|
|
}
|
|
unit -= 1;
|
|
}
|
|
return unit;
|
|
}
|
|
|
|
export function studiedToday(cards: number, secs: number): string {
|
|
const unit = naturalUnit(secs);
|
|
const amount = unitAmount(unit, secs);
|
|
const name = unitName(unit);
|
|
|
|
let secsPer = 0;
|
|
if (cards > 0) {
|
|
secsPer = secs / cards;
|
|
}
|
|
return tr.statisticsStudiedToday({
|
|
unit: name,
|
|
secsPerCard: secsPer,
|
|
cards,
|
|
amount,
|
|
});
|
|
}
|
|
|
|
function i18nFuncForUnit(
|
|
unit: TimespanUnit,
|
|
short: boolean,
|
|
): (_: { amount: number }) => string {
|
|
if (short) {
|
|
switch (unit) {
|
|
case TimespanUnit.Seconds:
|
|
return tr.statisticsElapsedTimeSeconds;
|
|
case TimespanUnit.Minutes:
|
|
return tr.statisticsElapsedTimeMinutes;
|
|
case TimespanUnit.Hours:
|
|
return tr.statisticsElapsedTimeHours;
|
|
case TimespanUnit.Days:
|
|
return tr.statisticsElapsedTimeDays;
|
|
case TimespanUnit.Months:
|
|
return tr.statisticsElapsedTimeMonths;
|
|
case TimespanUnit.Years:
|
|
return tr.statisticsElapsedTimeYears;
|
|
}
|
|
} else {
|
|
switch (unit) {
|
|
case TimespanUnit.Seconds:
|
|
return tr.schedulingTimeSpanSeconds;
|
|
case TimespanUnit.Minutes:
|
|
return tr.schedulingTimeSpanMinutes;
|
|
case TimespanUnit.Hours:
|
|
return tr.schedulingTimeSpanHours;
|
|
case TimespanUnit.Days:
|
|
return tr.schedulingTimeSpanDays;
|
|
case TimespanUnit.Months:
|
|
return tr.schedulingTimeSpanMonths;
|
|
case TimespanUnit.Years:
|
|
return tr.schedulingTimeSpanYears;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Describe the given seconds using the largest appropriate unit.
|
|
If precise is true, show to two decimal places, eg
|
|
eg 70 seconds -> "1.17 minutes"
|
|
If false, seconds and days are shown without decimals. */
|
|
export function timeSpan(seconds: number, short = false, precise = true): string {
|
|
const unit = naturalUnit(seconds);
|
|
let amount = unitAmount(unit, seconds);
|
|
if (!precise && unit < TimespanUnit.Months) {
|
|
amount = Math.round(amount);
|
|
}
|
|
return i18nFuncForUnit(unit, short)({ amount });
|
|
}
|
|
|
|
export function dayLabel(daysStart: number, daysEnd: number): string {
|
|
const larger = Math.max(Math.abs(daysStart), Math.abs(daysEnd));
|
|
const smaller = Math.min(Math.abs(daysStart), Math.abs(daysEnd));
|
|
if (larger - smaller <= 1) {
|
|
// singular
|
|
if (daysStart >= 0) {
|
|
return tr.statisticsInDaysSingle({ days: daysStart });
|
|
} else {
|
|
return tr.statisticsDaysAgoSingle({ days: -daysStart });
|
|
}
|
|
} else {
|
|
// range
|
|
if (daysStart >= 0) {
|
|
return tr.statisticsInDaysRange({
|
|
daysStart,
|
|
daysEnd: daysEnd - 1,
|
|
});
|
|
} else {
|
|
return tr.statisticsDaysAgoRange({
|
|
daysStart: Math.abs(daysEnd - 1),
|
|
daysEnd: -daysStart,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Helper for converting Unix timestamps to date strings. */
|
|
export class Timestamp {
|
|
private date: Date;
|
|
|
|
constructor(seconds: number) {
|
|
this.date = new Date(seconds * 1000);
|
|
}
|
|
|
|
/** YYYY-MM-DD */
|
|
dateString(): string {
|
|
const year = this.date.getFullYear();
|
|
const month = ("0" + (this.date.getMonth() + 1)).slice(-2);
|
|
const date = ("0" + this.date.getDate()).slice(-2);
|
|
return `${year}-${month}-${date}`;
|
|
}
|
|
|
|
/** HH:MM */
|
|
timeString(): string {
|
|
const hours = ("0" + this.date.getHours()).slice(-2);
|
|
const minutes = ("0" + this.date.getMinutes()).slice(-2);
|
|
return `${hours}:${minutes}`;
|
|
}
|
|
}
|