docs: tabbed settings, auto-update toggle and diagnostics

This commit is contained in:
Horizontal42 2026-07-03 12:03:58 +03:00
parent 1e06281f5e
commit 6f190c7245
4 changed files with 220 additions and 46 deletions

View file

@ -21,6 +21,8 @@ src/
index.ts SumEngine facade + extension API + sheet totals index.ts SumEngine facade + extension API + sheet totals
ui/ ui/
editor.ts CodeMirror 6 wiring: highlight, results overlay, copy editor.ts CodeMirror 6 wiring: highlight, results overlay, copy
search.ts cross-sheet search overlay (text + result-value queries)
export.ts renders the sheet + results to a PNG (clipboard/file)
app.css themes (light/dark via CSS variables) app.css themes (light/dark via CSS variables)
main.ts app bootstrap: documents, settings, status bar, dnd main.ts app bootstrap: documents, settings, status bar, dnd
storage.ts Tauri commands / localStorage fallback storage.ts Tauri commands / localStorage fallback
@ -29,8 +31,8 @@ src/
updater.ts checks tauri-plugin-updater, installs + relaunches updater.ts checks tauri-plugin-updater, installs + relaunches
src-tauri/ src-tauri/
src/main.rs tray, hide-to-tray, storage commands, rates fetching src/main.rs tray, hide-to-tray, storage commands, rates fetching
(open.er-api.com + CoinGecko + Yahoo Finance) with (open.er-api.com + CoinGecko + Yahoo Finance v8 chart)
cache, historical rates (frankfurter.app, permanent with cache, historical rates (frankfurter.dev, permanent
per-date cache), backups, data-folder migration, per-date cache), backups, data-folder migration,
file drops, plugin registration (autostart, file drops, plugin registration (autostart,
global-shortcut, single-instance, opener, dialog, global-shortcut, single-instance, opener, dialog,
@ -111,7 +113,49 @@ snapshot price) plus the mirror list in `src-tauri/src/main.rs`.
bypasses `formatValue` — the results overlay renders an SVG sparkline bypasses `formatValue` — the results overlay renders an SVG sparkline
instead of a text string. instead of a text string.
- The registry is rebuilt never; extensions mutate it (add phrases/functions) - The registry is rebuilt never; extensions mutate it (add phrases/functions)
at startup. at startup. `buildRegistry()` (`registry.ts`) is a thin orchestrator over
`registerCoreVocab` / `registerUnits` / `registerAreaVolume` /
`registerCurrencies` / `registerExtraUnits` / `registerCrypto` /
`registerCompletions` — **call order matters**, since `Registry.addPhrase`
keeps the *first* registration of an identical phrase and drops later ones
(core vocab must win over unit/currency phrases that happen to collide).
- Multiplying two same-dimension quantities (`2 kg * 500 g`) converts the
right operand into the left operand's unit before multiplying, so the
result keeps a sane unit (`1 kg`, not `1000` in a `kg` label). Cross-dimension
multiplication (`$12 * 3`, `5 hours * 2`) is unaffected — the right side is
treated as a plain scale factor there.
- A `//` only starts a comment at the start of a line or after whitespace
(`findCommentStart` in `index.ts`), so a URL like `see https://example.com`
isn't truncated at the double slash.
- **Date literals and formats.** Besides ISO (`2024-01-01`), the lexer accepts
`D.M.YYYY` / `M/D/YYYY` (`31.12.2024`, `12/31/2024`) with the same separator
on both sides and a 4-digit year. Day/month order is disambiguated per
literal: a component `>12` fixes which side is the day; if both are `≤12`,
the engine's `dateFormat` setting decides. All three literal forms reject
out-of-range components via a round-trip check (`validDateMs` in `lexer.ts`
— `new Date(y, mo-1, d)` silently *rolls over* invalid dates like
`2026-13-45` instead of erroring, so the constructed date's year/month/day
are compared back against the input). `SumEngine` resolves `dateFormat:
"system"` to an actual day/month order once via `detectDateOrder()`
(`datetime.ts`, probes `Intl.DateTimeFormat` on a `2000-12-31` fixture) and
passes it into `tokenize()` → `lexLine()` on every call; `"dmy"`/`"mdy"`
settings skip detection. Display works the same way in reverse: `formatter.ts`
either uses the OS-locale `Intl` formatting (`"system"`) or builds a fixed
`YYYY-MM-DD` / `DD.MM.YYYY` / `MM/DD/YYYY` string via
`Intl.DateTimeFormat(...).formatToParts()` (needed to respect the value's
own IANA timezone, which a hand-rolled `ms`-based formatter can't do).
`Intl.DateTimeFormat` instances are cached by options key in `formatter.ts`
(`dtf()`) since constructing one is comparatively expensive and every dated
line rebuilds its format on every render.
- Timezone name parsing (`time in Europe/Berlin`) has two independent traps.
First, `parseLine`'s noise-filter only keeps word tokens that immediately
follow a `conv` token (`in`/`as`/...); any other token type resets that
state — a bare `/` between two timezone words would normally drop the
second word before the parser ever sees it, so the filter special-cases a
`/` that appears while collecting post-conv words. Second, `parseTarget()`'s
word-collecting loop mirrors that same allowance so it keeps consuming past
the `/` instead of stopping at the first word. Both places need the
exception — fixing only one still drops `Europe/Berlin` down to `Europe`.
## Cross-sheet references ## Cross-sheet references
@ -147,10 +191,19 @@ recursing forever.
scroll/geometry changes. Click-to-copy reads `data-value`. scroll/geometry changes. Click-to-copy reads `data-value`.
- Engine evaluation is synchronous on every keystroke — a full sheet parse - Engine evaluation is synchronous on every keystroke — a full sheet parse
is well under a millisecond, so there's no debounce or worker. is well under a millisecond, so there's no debounce or worker.
- Search-all-sheets (`Ctrl+F`) is a plain line scan over `data.contents` in - Search-all-sheets (`Ctrl+F`) is a plain line scan over every open sheet's
`main.ts`, not a CodeMirror search extension — it has to cross documents, text, not a CodeMirror search extension — it has to cross documents, so it
so it never touches the active editor until a result is clicked never touches the active editor until a result is clicked
(`editor.goToLine`). (`editor.goToLine`). It lives in `src/ui/search.ts` (`initSearch()`), which
owns the `#search-overlay` DOM and its own 150 ms input debounce — a
result-value query (`>1000`) re-evaluates every open sheet, so debouncing
matters once there are more than a couple of sheets open.
- The settings popover is split into four tabs (General / Format / Data /
Updates) in `index.html`, each a `.settings-tabpanel` toggled by a
`#settings-tabs .tab` button in `bindSettingsUI()` (`main.ts`) — plain
`classList.toggle`, no router. Tab styling reuses the sidebar's
`doc-item.active` idiom (`background: var(--sel)` for the active pill)
rather than introducing a new visual pattern.
## Storage ## Storage
@ -165,7 +218,28 @@ recursing forever.
- `updater.ts` calls `tauri-plugin-updater`'s `check()` on boot; if a signed - `updater.ts` calls `tauri-plugin-updater`'s `check()` on boot; if a signed
update is available the user is prompted, then `downloadAndInstall()` + update is available the user is prompted, then `downloadAndInstall()` +
`tauri-plugin-process`'s `relaunch()`. `tauri-plugin-process`'s `relaunch()`. `checkForUpdate()` returns
`AvailableUpdate | null | "error"` — `null` means the check succeeded and
found nothing newer, `"error"` means the check itself failed (offline,
signature mismatch, etc.); the distinction only matters for the manual path
below. Every outcome is now visible in devtools: failures via
`console.warn`, and a successful "no update" result logs the running
version alongside "that's the latest published release" via `console.info`
— the most common false alarm is simply that the installed build already
*is* the newest release (e.g. right after installing it), which used to be
indistinguishable from a silently broken check.
- Closing the window hides it to the tray instead of exiting the process (see
`on_window_event` in `main.rs`), so a boot-time-only check can go unnoticed
for a long time on a machine that isn't restarted often. `main.ts` also
reruns `checkUpdate()` on a `setInterval` (every 6 hours) so a long-lived
tray process still notices new releases, and Settings → Updates has a
manual "Check for updates" button (`checkUpdate(true)`) that reports
"up to date" / "check failed" via toast — those toasts are suppressed on
the automatic background checks so the app doesn't nag every few hours.
A `SettingsData.autoUpdateEnabled` toggle turns off both the boot check and
the periodic recheck (re-read from `settings` on every interval tick, so
flipping it while the app is running takes effect immediately); the manual
button ignores the toggle and always works.
- The update endpoint is `latest.json` published alongside each GitHub - The update endpoint is `latest.json` published alongside each GitHub
Release. `release.yml` signs both architectures with a minisign keypair: Release. `release.yml` signs both architectures with a minisign keypair:
the public half is embedded in `tauri.conf.json`; the private half + its the public half is embedded in `tauri.conf.json`; the private half + its
@ -181,16 +255,19 @@ All files below live in `%APPDATA%/app.summarum.calc` (or the user-chosen folder
|------|----------|-----| |------|----------|-----|
| `settings.json` | app settings | persisted | | `settings.json` | app settings | persisted |
| `documents.json` | all sheets | persisted (400 ms debounce) | | `documents.json` | all sheets | persisted (400 ms debounce) |
| `rates.json` | live exchange rates | 1 hour | | `rates.json` | live exchange rates (open.er-api.com + CoinGecko) | 1 hour |
| `market.json` | stock/commodity prices | 15 minutes | | `market.json` | stock/commodity prices (Yahoo Finance v8 chart, one request per symbol) | 15 minutes |
| `rates-YYYY-MM-DD.json` | historical ECB rates for one date | permanent | | `rates-YYYY-MM-DD.json` | historical ECB rates for one date (frankfurter.dev) | permanent |
| `backups/documents-YYYY-MM-DD.json` | daily snapshots | 14 days | | `backups/documents-YYYY-MM-DD.json` | daily snapshots | 14 days |
| `backups/deleted/*.numi` | soft-deleted sheets | configurable | | `backups/deleted/*.numi` | soft-deleted sheets | configurable |
## Tests ## Tests
`npm test` runs 113 vitest cases over the engine (`src/engine/*.test.ts`): `npm test` runs 152 vitest cases: `src/engine/*.test.ts` covers every
every expression class, both languages, deterministic injected rates, expression class, both languages, deterministic injected rates, goal seek,
goal seek, historical rates (injected), plus a regression suite covering historical rates (injected), date-format literals/display (dedicated engines
all known-fixed bugs. UI is exercised manually; the engine is where the with an explicit `dateFormat`, since the default "system" format depends on
complexity lives. the CI machine's locale), plus a regression suite covering all known-fixed
bugs; `src/workspace.test.ts` covers cross-sheet references, caching and
cycle detection. UI is exercised manually; the engine is where the complexity
lives.

View file

@ -22,6 +22,8 @@ src/
index.ts фасад SumEngine + API расширений + итоги листа index.ts фасад SumEngine + API расширений + итоги листа
ui/ ui/
editor.ts обвязка CodeMirror 6: подсветка, оверлей результатов, копирование editor.ts обвязка CodeMirror 6: подсветка, оверлей результатов, копирование
search.ts оверлей поиска по всем листам (текст + запросы по значению)
export.ts рендерит лист + результаты в PNG (буфер обмена/файл)
app.css темы (светлая/тёмная через CSS-переменные) app.css темы (светлая/тёмная через CSS-переменные)
main.ts запуск приложения: документы, настройки, строка статуса, dnd main.ts запуск приложения: документы, настройки, строка статуса, dnd
storage.ts команды Tauri / запасной вариант на localStorage storage.ts команды Tauri / запасной вариант на localStorage
@ -30,10 +32,10 @@ src/
updater.ts проверка tauri-plugin-updater, установка + перезапуск updater.ts проверка tauri-plugin-updater, установка + перезапуск
src-tauri/ src-tauri/
src/main.rs трей, скрытие в трей, команды хранилища, загрузка курсов src/main.rs трей, скрытие в трей, команды хранилища, загрузка курсов
(open.er-api.com + CoinGecko + Yahoo Finance) с кэшем, (open.er-api.com + CoinGecko + Yahoo Finance v8 chart)
исторические курсы (frankfurter.app, постоянный кэш с кэшем, исторические курсы (frankfurter.dev, постоянный
на каждую дату), резервные копии, миграция папки данных, кэш на каждую дату), резервные копии, миграция папки
открытие файлов, регистрация плагинов (autostart, данных, открытие файлов, регистрация плагинов (autostart,
global-shortcut, single-instance, opener, dialog, global-shortcut, single-instance, opener, dialog,
updater, process) updater, process)
``` ```
@ -109,7 +111,53 @@ src-tauri/
- `ChartValue` (`kind: "chart"`) возвращается из `evalAgg("chart", …)` и - `ChartValue` (`kind: "chart"`) возвращается из `evalAgg("chart", …)` и
минует `formatValue` — оверлей результатов рисует SVG-спарклайн вместо текста. минует `formatValue` — оверлей результатов рисует SVG-спарклайн вместо текста.
- Реестр не пересобирается никогда; расширения мутируют его (добавляют фразы/функции) - Реестр не пересобирается никогда; расширения мутируют его (добавляют фразы/функции)
при запуске. при запуске. `buildRegistry()` (`registry.ts`) — тонкий оркестратор над
`registerCoreVocab` / `registerUnits` / `registerAreaVolume` /
`registerCurrencies` / `registerExtraUnits` / `registerCrypto` /
`registerCompletions` — **порядок вызовов важен**: `Registry.addPhrase`
оставляет *первую* регистрацию одинаковой фразы и отбрасывает последующие
(базовый словарь обязан побеждать фразы единиц/валют, если они случайно
совпадают).
- Умножение двух величин одного измерения (`2 kg * 500 g`) конвертирует
правый операнд в единицу левого перед умножением, чтобы результат имел
осмысленную единицу (`1 kg`, а не `1000` в подписи `kg`). Умножение разных
измерений (`$12 * 3`, `5 hours * 2`) не затронуто — там правая часть
трактуется как простой числовой множитель.
- `//` начинает комментарий только в начале строки или после пробела
(`findCommentStart` в `index.ts`), поэтому URL вида `see https://example.com`
не обрезается на двойном слэше.
- **Литералы и форматы дат.** Помимо ISO (`2024-01-01`), лексер понимает
`D.M.YYYY` / `M/D/YYYY` (`31.12.2024`, `12/31/2024`) с одинаковым
разделителем с обеих сторон и 4-значным годом. Порядок день/месяц
разрешается для каждого литерала отдельно: компонент `>12` однозначно
определяет, где день; если оба `≤12`, решает настройка движка `dateFormat`.
Все три формы литералов отклоняют компоненты вне диапазона через
round-trip проверку (`validDateMs` в `lexer.ts` — `new Date(y, mo-1, d)`
молча *перекатывает* невалидные даты вроде `2026-13-45` вместо ошибки,
поэтому построенные год/месяц/день сравниваются обратно со входом).
`SumEngine` разрешает `dateFormat: "system"` в реальный порядок день/месяц
один раз через `detectDateOrder()` (`datetime.ts`, пробует
`Intl.DateTimeFormat` на фикстуре `2000-12-31`) и передаёт его в
`tokenize()` → `lexLine()` при каждом вызове; настройки `"dmy"`/`"mdy"`
пропускают детекцию. Отображение работает так же в обратную сторону:
`formatter.ts` либо использует форматирование `Intl` по локали ОС
(`"system"`), либо строит фиксированную строку `YYYY-MM-DD` / `DD.MM.YYYY` /
`MM/DD/YYYY` через `Intl.DateTimeFormat(...).formatToParts()` (это нужно,
чтобы уважать собственный IANA-часовой пояс значения, чего форматтер на
основе голого `ms` сделать не может). Экземпляры `Intl.DateTimeFormat`
кэшируются по ключу опций в `formatter.ts` (`dtf()`), так как их создание
сравнительно дорого, а каждая строка с датой пересобирает формат при
каждой отрисовке.
- У разбора названий часовых поясов (`time in Europe/Berlin`) есть две
независимые ловушки. Во-первых, шумовой фильтр `parseLine` оставляет
word-токены только сразу после `conv`-токена (`in`/`as`/...); любой другой
тип токена сбрасывает это состояние — голый `/` между двумя словами
часового пояса обычно отбросил бы второе слово раньше, чем его увидит
парсер, поэтому фильтр отдельно разрешает `/`, встреченный при сборе
post-conv слов. Во-вторых, цикл сбора слов в `parseTarget()` зеркалит то же
исключение, чтобы продолжать читать после `/`, а не останавливаться на
первом слове. Нужны оба места — исправление только одного всё равно
урезает `Europe/Berlin` до `Europe`.
## Ссылки между листами ## Ссылки между листами
@ -144,10 +192,20 @@ src-tauri/
прокрутке/изменении геометрии. Клик для копирования читает `data-value`. прокрутке/изменении геометрии. Клик для копирования читает `data-value`.
- Оценка движка синхронная на каждое нажатие клавиши — полный разбор листа - Оценка движка синхронная на каждое нажатие клавиши — полный разбор листа
занимает менее миллисекунды, поэтому нет дебаунса и воркеров. занимает менее миллисекунды, поэтому нет дебаунса и воркеров.
- Поиск по всем листам (`Ctrl+F`) — простое построчное сканирование - Поиск по всем листам (`Ctrl+F`) — простое построчное сканирование текста
`data.contents` в `main.ts`, а не расширение поиска CodeMirror — ему нужно каждого открытого листа, а не расширение поиска CodeMirror — ему нужно
пересекать документы, поэтому он не трогает активный редактор, пока не пересекать документы, поэтому он не трогает активный редактор, пока не
кликнут результат (`editor.goToLine`). кликнут результат (`editor.goToLine`). Живёт в `src/ui/search.ts`
(`initSearch()`), который владеет DOM `#search-overlay` и собственным
дебаунсом ввода на 150 мс — запрос по значению (`>1000`) пересчитывает
каждый открытый лист, так что дебаунс важен, как только листов становится
больше пары.
- Поповер настроек разбит на четыре вкладки (Общие / Формат / Данные /
Обновления) в `index.html`, каждая — `.settings-tabpanel`, переключаемая
кнопкой `#settings-tabs .tab` в `bindSettingsUI()` (`main.ts`) — обычный
`classList.toggle`, без роутера. Стиль вкладок повторяет идиому сайдбара
`doc-item.active` (`background: var(--sel)` для активной таблетки) вместо
нового визуального паттерна.
## Хранилище ## Хранилище
@ -163,6 +221,28 @@ src-tauri/
- `updater.ts` вызывает `check()` из `tauri-plugin-updater` при запуске; если - `updater.ts` вызывает `check()` из `tauri-plugin-updater` при запуске; если
подписанное обновление доступно, пользователю предлагают установить, затем подписанное обновление доступно, пользователю предлагают установить, затем
`downloadAndInstall()` + `relaunch()` из `tauri-plugin-process`. `downloadAndInstall()` + `relaunch()` из `tauri-plugin-process`.
`checkForUpdate()` возвращает `AvailableUpdate | null | "error"` — `null`
значит проверка прошла успешно и ничего новее нет, `"error"` значит сама
проверка не удалась (офлайн, рассинхрон подписи и т.п.); различие важно
только для ручного пути ниже. Теперь виден любой исход: сбои через
`console.warn`, а успешный результат «нет обновления» логирует текущую
версию рядом с «это последний опубликованный релиз» через `console.info` —
самый частый ложный сигнал тревоги это когда установленная сборка И ЕСТЬ
последний релиз (например, сразу после его установки), что раньше было
неотличимо от молча сломанной проверки.
- Закрытие окна прячет его в трей вместо завершения процесса (см.
`on_window_event` в `main.rs`), поэтому проверка только при старте может
долго оставаться незамеченной на машине, которую редко перезагружают.
`main.ts` также перезапускает `checkUpdate()` через `setInterval` (каждые
6 часов), чтобы долгоживущий трей-процесс всё равно замечал новые релизы,
а в настройках (вкладка «Обновления») есть кнопка «Проверить обновления»
(`checkUpdate(true)`), которая сообщает «последняя версия» / «не удалось
проверить» через toast — эти уведомления подавлены на автоматических
фоновых проверках, чтобы приложение не напоминало о себе каждые несколько
часов. Тумблер `SettingsData.autoUpdateEnabled` отключает и проверку при
запуске, и периодическую (значение перечитывается из `settings` на каждом
тике интервала, так что переключение прямо во время работы срабатывает
сразу); ручная кнопка тумблер игнорирует и работает всегда.
- Эндпоинт обновлений — `latest.json`, публикуемый вместе с каждым GitHub - Эндпоинт обновлений — `latest.json`, публикуемый вместе с каждым GitHub
Release. `release.yml` подписывает обе архитектуры парой ключей minisign: Release. `release.yml` подписывает обе архитектуры парой ключей minisign:
публичная половина зашита в `tauri.conf.json`; приватная половина + пароль публичная половина зашита в `tauri.conf.json`; приватная половина + пароль
@ -179,16 +259,19 @@ src-tauri/
|------|-----------|-----| |------|-----------|-----|
| `settings.json` | настройки | постоянно | | `settings.json` | настройки | постоянно |
| `documents.json` | все листы | постоянно (дебаунс 400 мс) | | `documents.json` | все листы | постоянно (дебаунс 400 мс) |
| `rates.json` | живые курсы валют | 1 час | | `rates.json` | живые курсы валют (open.er-api.com + CoinGecko) | 1 час |
| `market.json` | котировки акций/сырья | 15 минут | | `market.json` | котировки акций/сырья (Yahoo Finance v8 chart, по запросу на символ) | 15 минут |
| `rates-YYYY-MM-DD.json` | исторические курсы ЕЦБ за одну дату | навсегда | | `rates-YYYY-MM-DD.json` | исторические курсы ЕЦБ за одну дату (frankfurter.dev) | навсегда |
| `backups/documents-YYYY-MM-DD.json` | ежедневные снимки | 14 дней | | `backups/documents-YYYY-MM-DD.json` | ежедневные снимки | 14 дней |
| `backups/deleted/*.numi` | мягко удалённые листы | настраивается | | `backups/deleted/*.numi` | мягко удалённые листы | настраивается |
## Тесты ## Тесты
`npm test` запускает 113 vitest-кейсов по движку (`src/engine/*.test.ts`): `npm test` запускает 152 vitest-кейса: `src/engine/*.test.ts` покрывает все
все классы выражений, оба языка, детерминированные инжектированные курсы, классы выражений, оба языка, детерминированные инжектированные курсы, поиск
поиск неизвестного, исторические курсы (инжектированные), плюс набор регрессий неизвестного, исторические курсы (инжектированные), литералы и отображение
по всем известным исправленным багам. UI проверяется вручную; движок — там форматов дат (отдельные движки с явным `dateFormat` — формат «system» по
где живёт вся сложность. умолчанию зависит от локали машины CI), плюс набор регрессий по всем известным
исправленным багам; `src/workspace.test.ts` покрывает межлистовые ссылки,
кэширование и обнаружение циклов. UI проверяется вручную; движок — там где
живёт вся сложность.

View file

@ -67,13 +67,16 @@ the math is ignored, so `spent 20 on pizza and 5 on coffee` just gives 25.
`100 AAPL in USD`, `2 GC=F in USD` (gold futures), `MSFT + GOOGL`. `100 AAPL in USD`, `2 GC=F in USD` (gold futures), `MSFT + GOOGL`.
**Historical exchange rates** — `1000 USD in EUR on 2024-01-01` looks up the **Historical exchange rates** — `1000 USD in EUR on 2024-01-01` looks up the
ECB rate for that exact date (via frankfurter.app) and caches it permanently. ECB rate for that exact date (via frankfurter.dev) and caches it permanently.
**Percents the human way** — `20% of 80`, `20% off 1500`, `5 as a % of 25`, **Percents the human way** — `20% of 80`, `20% off 1500`, `5 as a % of 25`,
`20% of what is 30`. `20% of what is 30`.
**Dates and time** — `today + 2 weeks`, `tomorrow - today`, `time in New York`, **Dates and time** — `today + 2 weeks`, `tomorrow - today`, `time in New York`
`now in unix`, `1750000000 as date`. ISO date literals: `2024-01-01 + 30 days`, (also works with a raw IANA id: `time in Europe/Berlin`), `now in unix`,
`1750000000 as date`. Date literals: ISO (`2024-01-01 + 30 days`), or
dot/slash with a 4-digit year — `31.12.2024`, `12/31/2024` — where the
day/month order for ambiguous cases follows the Date format setting.
`days until 2027-01-01`, `days since 2020-03-15`. `days until 2027-01-01`, `days since 2020-03-15`.
**Goal seek** — put `?` where the unknown is and an `=` with the target: **Goal seek** — put `?` where the unknown is and an `=` with the target:
@ -117,9 +120,13 @@ auto-derived one) so the reference doesn't break when its first line changes.
- **Data folder is configurable** — point it at OneDrive/Dropbox and your - **Data folder is configurable** — point it at OneDrive/Dropbox and your
sheets follow you between machines. sheets follow you between machines.
- **Always on top** (Settings) pins the window above everything else. - **Always on top** (Settings) pins the window above everything else.
- The app checks for updates on launch and offers to install them — no - The app checks for updates on launch and every few hours after that, and
manual download-and-reinstall. offers to install them — no manual download-and-reinstall. Settings →
- Light/dark theme, en/ru interface, configurable precision and separators. Updates → "Check for updates" runs one on demand, or turn automatic
checking off entirely with the toggle next to it.
- Light/dark theme, en/ru interface, configurable precision, separators, and
date format (system locale, `31.12.2024`, `12/31/2024`, or `2024-12-31`).
Settings is split into General / Format / Data / Updates tabs.
## Extensions ## Extensions
@ -149,7 +156,7 @@ see [ARCHITECTURE.md](ARCHITECTURE.md).
```bash ```bash
npm install npm install
npm test # engine tests (113 cases) npm test # engine tests (152 cases)
npm run tauri dev # run the app npm run tauri dev # run the app
npm run tauri build # build the installer npm run tauri build # build the installer
``` ```

View file

@ -63,13 +63,16 @@ AAPL в USD → $213.55
`100 AAPL в USD`, `2 GC=F в USD` (фьючерс на золото), `MSFT + GOOGL`. `100 AAPL в USD`, `2 GC=F в USD` (фьючерс на золото), `MSFT + GOOGL`.
**Исторические курсы** — `1000 USD в EUR на 2024-01-01` запрашивает курс ЕЦБ **Исторические курсы** — `1000 USD в EUR на 2024-01-01` запрашивает курс ЕЦБ
за этот день (через frankfurter.app) и кэширует его навсегда. за этот день (через frankfurter.dev) и кэширует его навсегда.
**Проценты по-человечески** — `20% от 80`, `скидка 20% от 1500`, `5 это сколько % от 25`, **Проценты по-человечески** — `20% от 80`, `скидка 20% от 1500`, `5 это сколько % от 25`,
`20% от какого числа равно 30`. `20% от какого числа равно 30`.
**Даты и время** — `сегодня + 2 недели`, `завтра - сегодня`, `время в Нью-Йорке`, **Даты и время** — `сегодня + 2 недели`, `завтра - сегодня`, `время в Нью-Йорке`
`сейчас в unix`, `1750000000 как дата`. ISO-даты: `2024-01-01 + 30 дней`, (работает и с прямым IANA-id: `время в Europe/Berlin`), `сейчас в unix`,
`1750000000 как дата`. Форматы дат: ISO (`2024-01-01 + 30 дней`), или через
точку/слэш с 4-значным годом — `31.12.2024`, `12/31/2024` — порядок день/месяц
для неоднозначных случаев берётся из настройки «Формат даты».
`дней до 2027-01-01`, `дней с 2020-03-15`. `дней до 2027-01-01`, `дней с 2020-03-15`.
**Поиск неизвестного** — поставь `?` на место неизвестного и `=` с нужным результатом: **Поиск неизвестного** — поставь `?` на место неизвестного и `=` с нужным результатом:
@ -112,9 +115,13 @@ AAPL в USD → $213.55
- **Папка данных настраивается** — укажи OneDrive/Dropbox и листы синхронизируются - **Папка данных настраивается** — укажи OneDrive/Dropbox и листы синхронизируются
между машинами. между машинами.
- **Поверх всех окон** (в настройках) закрепляет окно над остальными. - **Поверх всех окон** (в настройках) закрепляет окно над остальными.
- Приложение само проверяет обновления при запуске и предлагает установить — - Приложение проверяет обновления при запуске и потом каждые несколько
вручную скачивать и переустанавливать не нужно. часов, предлагая установить — вручную скачивать и переустанавливать не
- Светлая/тёмная тема, интерфейс en/ru, настраиваемая точность и разделители. нужно. Настройки → Обновления → «Проверить обновления» запускает проверку
вручную, либо отключи автопроверку целиком тумблером рядом.
- Светлая/тёмная тема, интерфейс en/ru, настраиваемая точность, разделители
и формат даты (системный, `31.12.2024`, `12/31/2024` или `2024-12-31`).
Настройки разбиты на вкладки: Общие / Формат / Данные / Обновления.
## Расширения ## Расширения
@ -144,7 +151,7 @@ numi.addFunction({ id: "гипот", phrases: "гипот" },
```bash ```bash
npm install npm install
npm test # тесты движка (113 кейсов) npm test # тесты движка (152 кейса)
npm run tauri dev # запустить приложение npm run tauri dev # запустить приложение
npm run tauri build # собрать установщик npm run tauri build # собрать установщик
``` ```