Skip to content

Commit 75107f7

Browse files
feat: add math notebook (#677)
* feat: add math notebook * refactor: reorganize math-notebook composables * refactor(i18n): update math-notebook namespace to mathNotebook and standardize keys * feat(currency): remove fallback rates and handle unavailability * feat(math-engine): add support for dotted date parsing and assignments * feat(math-editor): add syntax highlighting & fix scroll * feat: new style copy interactions with hover effects * feat: improve UI interactions * refactor(math-engine): rename constants to mc/math for rebranding * refactor: update date formatting in SheetList to use date-fns * feat: add help button to open documentation
1 parent f58a574 commit 75107f7

34 files changed

Lines changed: 4956 additions & 6 deletions

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"copy:locales": "node scripts/copy-locales.js",
3434
"bench:load-test": "node scripts/bench-load-test.js",
3535
"bench:seed": "node scripts/bench-seed.js",
36+
"test": "vitest run",
37+
"test:watch": "vitest",
3638
"lint": "eslint .",
3739
"lint:fix": "eslint --fix .",
3840
"release": "bumpp -c 'build: release v' -t",
@@ -76,6 +78,7 @@
7678
"marked": "^15.0.8",
7779
"markmap-lib": "^0.18.11",
7880
"markmap-view": "^0.18.10",
81+
"mathjs": "^15.1.1",
7982
"mermaid": "^11.6.0",
8083
"nanoid": "^3.3.8",
8184
"nearest-color": "^0.4.4",
@@ -128,6 +131,7 @@
128131
"unplugin-auto-import": "^19.1.0",
129132
"unplugin-vue-components": "^28.4.0",
130133
"vite": "^6.1.1",
134+
"vitest": "^4.0.18",
131135
"vue": "^3.5.13",
132136
"vue-router": "^4.5.0"
133137
},

pnpm-lock.yaml

Lines changed: 336 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/currencyRates.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { store } from './store'
2+
3+
const CACHE_TTL = 1000 * 60 * 60
4+
5+
interface CurrencyRatesApiResponse {
6+
result?: string
7+
rates?: Record<string, number>
8+
time_last_update_unix?: number
9+
}
10+
11+
export interface CurrencyRatesPayload {
12+
rates: Record<string, number>
13+
fetchedAt: number
14+
source: 'live' | 'cache' | 'unavailable'
15+
}
16+
17+
function normalizeRates(rates: Record<string, number>) {
18+
const normalized: Record<string, number> = { USD: 1 }
19+
20+
Object.entries(rates).forEach(([code, value]) => {
21+
if (typeof value === 'number' && Number.isFinite(value)) {
22+
normalized[code] = value
23+
}
24+
})
25+
26+
normalized.USD = 1
27+
28+
return normalized
29+
}
30+
31+
export async function getCurrencyRates(): Promise<CurrencyRatesPayload> {
32+
const cached = store.currencyRates.get('cache')
33+
34+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
35+
return {
36+
...cached,
37+
source: 'cache',
38+
}
39+
}
40+
41+
try {
42+
const response = await fetch('https://open.er-api.com/v6/latest/USD')
43+
if (!response.ok) {
44+
throw new Error(
45+
`Currency rates request failed with status ${response.status}`,
46+
)
47+
}
48+
49+
const data = (await response.json()) as CurrencyRatesApiResponse
50+
if (data.result !== 'success' || !data.rates) {
51+
throw new Error('Currency rates response is invalid')
52+
}
53+
54+
const payload = {
55+
rates: normalizeRates(data.rates),
56+
fetchedAt: data.time_last_update_unix
57+
? data.time_last_update_unix * 1000
58+
: Date.now(),
59+
}
60+
61+
store.currencyRates.set('cache', payload)
62+
63+
return {
64+
...payload,
65+
source: 'live',
66+
}
67+
}
68+
catch {
69+
if (cached) {
70+
return {
71+
...cached,
72+
source: 'cache',
73+
}
74+
}
75+
76+
return {
77+
rates: {},
78+
fetchedAt: 0,
79+
source: 'unavailable',
80+
}
81+
}
82+
}

src/main/i18n/locales/en_US/menu.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"update": "Check for Updates...",
77
"quit": "Quit massCode",
88
"about": "About massCode",
9-
"hide": "Hide massCode"
9+
"hide": "Hide massCode",
10+
"mathNotebook": "Math Notebook"
1011
},
1112
"help": {
1213
"label": "Help",

src/main/i18n/locales/en_US/ui.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,19 @@
114114
"selectedMultiple": "{{count}} Snippets Selected",
115115
"noSelected": "No Snippet Selected"
116116
},
117+
"mathNotebook": {
118+
"label": "Math Notebook",
119+
"sheetList": "Sheet List",
120+
"newSheet": "New Sheet",
121+
"untitled": "Untitled",
122+
"copied": "Copied",
123+
"currencyUnavailable": "Currency rates service unavailable"
124+
},
117125
"placeholder": {
118126
"emptyTagList": "Add tags to snippets to see them here",
119127
"emptyFoldersList": "No Folders",
120128
"emptySnippetsList": "No Snippets",
129+
"emptySheetList": "No Sheets",
121130
"search": "Search",
122131
"addTag": "Add Tag"
123132
}

src/main/ipc/handlers/system.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { app, ipcMain, shell } from 'electron'
2+
import { getCurrencyRates } from '../../currencyRates'
23

34
export function registerSystemHandlers() {
5+
ipcMain.handle('system:currency-rates', () => {
6+
return getCurrencyRates()
7+
})
8+
49
ipcMain.handle('system:reload', () => {
510
app.relaunch()
611
app.quit()

src/main/menu/main.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ const appMenuItems: MenuConfig[] = [
9393
accelerator: 'CommandOrControl+.',
9494
click: () => send('main-menu:goto-devtools'),
9595
},
96+
{
97+
id: 'math-notebook',
98+
label: i18n.t('menu:app.mathNotebook'),
99+
accelerator: 'CommandOrControl+Shift+.',
100+
click: () => send('main-menu:goto-math-notebook'),
101+
},
96102
{
97103
type: 'separator' as any,
98104
},

src/main/preload.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { AppStore, PreferencesStore } from './store/types'
1+
import type {
2+
AppStore,
3+
MathNotebookStore,
4+
PreferencesStore,
5+
} from './store/types'
26
import type { EventCallback } from './types'
37
import type { Channel } from './types/ipc'
48
import { contextBridge, ipcRenderer } from 'electron'
@@ -39,6 +43,15 @@ contextBridge.exposeInMainWorld('electron', {
3943
) => store.preferences.set(name, value),
4044
delete: (name: keyof PreferencesStore) => store.preferences.delete(name),
4145
},
46+
mathNotebook: {
47+
get: (name: keyof MathNotebookStore) => store.mathNotebook.get(name),
48+
set: <T extends keyof MathNotebookStore>(
49+
name: T,
50+
value: MathNotebookStore[T],
51+
) => store.mathNotebook.set(name, value),
52+
delete: (name: keyof MathNotebookStore) =>
53+
store.mathNotebook.delete(name),
54+
},
4255
},
4356
i18n: {
4457
t: (key: string, options?: any) => i18n.t(key, options),

src/main/store/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import app from './module/app'
2+
import currencyRates from './module/currency-rates'
3+
import mathNotebook from './module/math-notebook'
24
import preferences from './module/preferences'
35

46
export const store = {
57
app,
8+
currencyRates,
69
preferences,
10+
mathNotebook,
711
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { CurrencyRatesStore } from '../types'
2+
import Store from 'electron-store'
3+
4+
export default new Store<CurrencyRatesStore>({
5+
name: 'currency-rates',
6+
cwd: 'v2',
7+
8+
defaults: {
9+
cache: null,
10+
},
11+
})

0 commit comments

Comments
 (0)