Skip to content

Commit ddc9aef

Browse files
committed
refactor: modularize notes tool by extracting database operations, PDF utilities, and types into separate files, and add error handling.
1 parent 30e00d8 commit ddc9aef

File tree

4 files changed

+167
-119
lines changed

4 files changed

+167
-119
lines changed

src/tools/notes/db.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { Note } from './types.ts';
2+
3+
const DB_NAME = 'NotesDB';
4+
const STORE_NAME = 'notes';
5+
const DB_VERSION = 1;
6+
7+
export function openDB(): Promise<IDBDatabase> {
8+
return new Promise((resolve, reject) => {
9+
const request = indexedDB.open(DB_NAME, DB_VERSION);
10+
request.onerror = () => reject(request.error);
11+
request.onsuccess = () => resolve(request.result);
12+
request.onupgradeneeded = (event) => {
13+
const db = (event.target as IDBOpenDBRequest).result;
14+
if (!db.objectStoreNames.contains(STORE_NAME)) {
15+
db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
16+
}
17+
};
18+
});
19+
}
20+
21+
export async function getAllNotes(db: IDBDatabase): Promise<Note[]> {
22+
return new Promise((resolve, reject) => {
23+
const transaction = db.transaction(STORE_NAME, 'readonly');
24+
const store = transaction.objectStore(STORE_NAME);
25+
const request = store.getAll();
26+
request.onsuccess = () => resolve(request.result);
27+
request.onerror = () => reject(request.error);
28+
});
29+
}
30+
31+
export async function getNoteById(db: IDBDatabase, id: number): Promise<Note | undefined> {
32+
return new Promise((resolve, reject) => {
33+
const transaction = db.transaction(STORE_NAME, 'readonly');
34+
const store = transaction.objectStore(STORE_NAME);
35+
const request = store.get(id);
36+
request.onsuccess = () => resolve(request.result);
37+
request.onerror = () => reject(request.error);
38+
});
39+
}
40+
41+
export async function saveNote(db: IDBDatabase, content: string, editingId: number | null): Promise<void> {
42+
return new Promise((resolve, reject) => {
43+
const transaction = db.transaction(STORE_NAME, 'readwrite');
44+
const store = transaction.objectStore(STORE_NAME);
45+
46+
if (editingId !== null) {
47+
const request = store.get(editingId);
48+
request.onsuccess = () => {
49+
const note = request.result;
50+
if (note) {
51+
note.content = content;
52+
note.updatedAt = Date.now();
53+
store.put(note);
54+
}
55+
};
56+
} else {
57+
const note: Note = {
58+
content,
59+
createdAt: Date.now(),
60+
};
61+
store.add(note);
62+
}
63+
64+
transaction.oncomplete = () => resolve();
65+
transaction.onerror = () => reject(transaction.error);
66+
});
67+
}
68+
69+
export async function deleteNote(db: IDBDatabase, id: number): Promise<void> {
70+
return new Promise((resolve, reject) => {
71+
const transaction = db.transaction(STORE_NAME, 'readwrite');
72+
const store = transaction.objectStore(STORE_NAME);
73+
store.delete(id);
74+
transaction.oncomplete = () => resolve();
75+
transaction.onerror = () => reject(transaction.error);
76+
});
77+
}

src/tools/notes/index.ts

Lines changed: 40 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,10 @@
11
import OverType from 'overtype';
22
import { MarkdownParser } from 'overtype/parser';
33
import { isDarkMode } from '../../js/theme.ts';
4-
import { downloadFile } from '../../js/file-utils.ts';
5-
import { htmlToPdfBuffer } from '../../js/mupdf-utils.ts';
64
import { showMessage } from '../../js/ui.ts';
7-
8-
interface Note {
9-
id?: number;
10-
content: string;
11-
createdAt: number;
12-
updatedAt?: number;
13-
}
14-
15-
const DB_NAME = 'NotesDB';
16-
const STORE_NAME = 'notes';
17-
const DB_VERSION = 1;
18-
19-
function openDB(): Promise<IDBDatabase> {
20-
return new Promise((resolve, reject) => {
21-
const request = indexedDB.open(DB_NAME, DB_VERSION);
22-
request.onerror = () => reject(request.error);
23-
request.onsuccess = () => resolve(request.result);
24-
request.onupgradeneeded = (event) => {
25-
const db = (event.target as IDBOpenDBRequest).result;
26-
if (!db.objectStoreNames.contains(STORE_NAME)) {
27-
db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
28-
}
29-
};
30-
});
31-
}
5+
import { openDB, getAllNotes, saveNote, deleteNote, getNoteById } from './db.ts';
6+
import { removeMarkdownSyntax, exportNoteToPdf } from './pdf-utils.ts';
7+
import type { Note } from './types.ts';
328

339
// noinspection JSUnusedGlobalSymbols
3410
export default async function init() {
@@ -48,12 +24,8 @@ export default async function init() {
4824
let editingId: number | null = null;
4925

5026
async function loadNotes(query = '') {
51-
const transaction = db.transaction(STORE_NAME, 'readonly');
52-
const store = transaction.objectStore(STORE_NAME);
53-
const request = store.getAll();
54-
55-
request.onsuccess = () => {
56-
let notes: Note[] = request.result;
27+
try {
28+
let notes = await getAllNotes(db);
5729

5830
if (query) {
5931
const q = query.toLowerCase();
@@ -62,7 +34,10 @@ export default async function init() {
6234

6335
notes.sort((a, b) => (b.updatedAt || b.createdAt) - (a.updatedAt || a.createdAt));
6436
renderNotes(notes);
65-
};
37+
} catch (e) {
38+
console.error('Failed to load notes:', e);
39+
showMessage('Failed to load notes.', { type: 'alert' });
40+
}
6641
}
6742

6843
function renderNotes(notes: Note[]) {
@@ -117,33 +92,18 @@ export default async function init() {
11792
.join('');
11893
}
11994

120-
async function saveNote() {
95+
async function handleSave() {
12196
const content = overType.getValue().trim();
12297
if (!content) return;
12398

124-
const transaction = db.transaction(STORE_NAME, 'readwrite');
125-
const store = transaction.objectStore(STORE_NAME);
126-
127-
if (editingId !== null) {
128-
const request = store.get(editingId);
129-
request.onsuccess = () => {
130-
const note = request.result;
131-
note.content = content;
132-
note.updatedAt = Date.now();
133-
store.put(note);
134-
};
135-
} else {
136-
const note: Note = {
137-
content,
138-
createdAt: Date.now(),
139-
};
140-
store.add(note);
141-
}
142-
143-
transaction.oncomplete = () => {
99+
try {
100+
await saveNote(db, content, editingId);
144101
resetForm();
145102
loadNotes(searchInput.value);
146-
};
103+
} catch (e) {
104+
console.error('Failed to save note:', e);
105+
showMessage('Failed to save note.', { type: 'alert' });
106+
}
147107
}
148108

149109
function resetForm() {
@@ -155,12 +115,8 @@ export default async function init() {
155115
}
156116

157117
async function startEdit(id: number) {
158-
const transaction = db.transaction(STORE_NAME, 'readonly');
159-
const store = transaction.objectStore(STORE_NAME);
160-
const request = store.get(id);
161-
162-
request.onsuccess = () => {
163-
const note = request.result;
118+
try {
119+
const note = await getNoteById(db, id);
164120
if (note) {
165121
editingId = id;
166122
overType.setValue(note.content);
@@ -170,71 +126,36 @@ export default async function init() {
170126
overType.focus();
171127
window.scrollTo({ top: 0, behavior: 'smooth' });
172128
}
173-
};
129+
} catch (e) {
130+
console.error('Failed to load note for editing:', e);
131+
showMessage('Failed to load note for editing.', { type: 'alert' });
132+
}
174133
}
175134

176-
async function deleteNote(id: number) {
177-
const transaction = db.transaction(STORE_NAME, 'readwrite');
178-
const store = transaction.objectStore(STORE_NAME);
179-
store.delete(id);
180-
transaction.oncomplete = () => {
135+
async function handleDelete(id: number) {
136+
try {
137+
await deleteNote(db, id);
181138
if (editingId === id) resetForm();
182139
loadNotes(searchInput.value);
183-
};
184-
}
185-
186-
const removeMarkdownSyntax = (html: string) => {
187-
let htmlContent = html.replace(/<span class="syntax-marker[^"]*">.*?<\/span>/g, "");
188-
htmlContent = htmlContent.replace(/\sclass="(bullet-list|ordered-list|code-fence|hr-marker|blockquote|url-part)"/g, "");
189-
htmlContent = htmlContent.replace(/\sclass=""/g, "");
190-
return htmlContent;
140+
} catch (e) {
141+
console.error('Failed to delete note:', e);
142+
showMessage('Failed to delete note.', { type: 'alert' });
143+
}
191144
}
192145

193-
async function exportToPdf(id: number) {
194-
const transaction = db.transaction(STORE_NAME, 'readonly');
195-
const store = transaction.objectStore(STORE_NAME);
196-
const request = store.get(id);
197-
198-
request.onsuccess = async () => {
199-
const note = request.result;
146+
async function handleExport(id: number) {
147+
try {
148+
const note = await getNoteById(db, id);
200149
if (note) {
201-
// Use preview mode to output clean HTML without markdown syntax markers
202-
let htmlContent = MarkdownParser.parse(note.content);
203-
htmlContent = removeMarkdownSyntax(htmlContent);
204-
const fullHtml = `<!DOCTYPE html>
205-
<html>
206-
<head>
207-
<style>
208-
body { font-family: sans-serif; padding: 20px; line-height: 1.5; color: #000; background: #fff; }
209-
h1, h2, h3 { font-weight: bold; margin-top: 0.5em; margin-bottom: 0.2em; }
210-
h1 { font-size: 1.5em; }
211-
h2 { font-size: 1.25em; }
212-
h3 { font-size: 1.1em; }
213-
ul, ol { margin-left: 0; padding-left: 20px; }
214-
.blockquote { display: block; border-left: 4px solid #ccc; padding-left: 1em; margin: 0.5em 0; opacity: 0.8; }
215-
code { background-color: #f0f0f0; padding: 0.1em 0.2em; border-radius: 0.2em; font-size: 0.9em; }
216-
.code-block { background-color: #f0f0f0; padding: 1em; border-radius: 0.5em; margin: 1em 0; overflow-x: auto; white-space: pre; }
217-
.code-fence { opacity: 0.3; font-size: 0.8em; }
218-
a { color: #0000ee; text-decoration: underline; }
219-
</style>
220-
</head>
221-
<body>
222-
${htmlContent}
223-
</body>
224-
</html>`;
225-
226-
try {
227-
const pdfBytes = await htmlToPdfBuffer(fullHtml);
228-
await downloadFile(pdfBytes, `note-${id}.pdf`, "application/pdf");
229-
} catch (e) {
230-
console.error("Failed to export PDF:", e);
231-
showMessage('Failed to export PDF. See console for details.', { type: 'alert' });
232-
}
150+
await exportNoteToPdf(id, note.content);
233151
}
234-
};
152+
} catch (e) {
153+
console.error('Failed to export note:', e);
154+
showMessage('Failed to export note.', { type: 'alert' });
155+
}
235156
}
236157

237-
addBtn.addEventListener('click', saveNote);
158+
addBtn.addEventListener('click', handleSave);
238159
cancelBtn.addEventListener('click', resetForm);
239160
searchInput.addEventListener('input', () => loadNotes(searchInput.value));
240161

@@ -265,12 +186,12 @@ export default async function init() {
265186
} else if (deleteBtn) {
266187
const id = parseInt(deleteBtn.getAttribute('data-id') || '0');
267188
if (id && confirm('Delete this note?')) {
268-
deleteNote(id);
189+
handleDelete(id);
269190
}
270191
} else if (target.closest('.export-btn')) {
271192
const btn = target.closest('.export-btn') as HTMLButtonElement;
272193
const id = parseInt(btn.getAttribute('data-id') || '0');
273-
if (id) exportToPdf(id);
194+
if (id) handleExport(id);
274195
}
275196
});
276197

src/tools/notes/pdf-utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { downloadFile } from '../../js/file-utils.ts';
2+
import { htmlToPdfBuffer } from '../../js/mupdf-utils.ts';
3+
import { showMessage } from '../../js/ui.ts';
4+
import { MarkdownParser } from 'overtype/parser';
5+
6+
export const removeMarkdownSyntax = (html: string): string => {
7+
let htmlContent = html.replace(/<span class="syntax-marker[^"]*">.*?<\/span>/g, '');
8+
htmlContent = htmlContent.replace(/\sclass="(bullet-list|ordered-list|code-fence|hr-marker|blockquote|url-part)"/g, '');
9+
htmlContent = htmlContent.replace(/\sclass=""/g, '');
10+
return htmlContent;
11+
};
12+
13+
export async function exportNoteToPdf(id: number, content: string): Promise<void> {
14+
const htmlContent = removeMarkdownSyntax(MarkdownParser.parse(content));
15+
const fullHtml = `<!DOCTYPE html>
16+
<html>
17+
<head>
18+
<style>
19+
body { font-family: sans-serif; padding: 20px; line-height: 1.5; color: #000; background: #fff; }
20+
h1, h2, h3 { font-weight: bold; margin-top: 0.5em; margin-bottom: 0.2em; }
21+
h1 { font-size: 1.5em; }
22+
h2 { font-size: 1.25em; }
23+
h3 { font-size: 1.1em; }
24+
ul, ol { margin-left: 0; padding-left: 20px; }
25+
.blockquote { display: block; border-left: 4px solid #ccc; padding-left: 1em; margin: 0.5em 0; opacity: 0.8; }
26+
code { background-color: #f0f0f0; padding: 0.1em 0.2em; border-radius: 0.2em; font-size: 0.9em; }
27+
.code-block { background-color: #f0f0f0; padding: 1em; border-radius: 0.5em; margin: 1em 0; overflow-x: auto; white-space: pre; }
28+
.code-fence { opacity: 0.3; font-size: 0.8em; }
29+
a { color: #0000ee; text-decoration: underline; }
30+
</style>
31+
</head>
32+
<body>
33+
${htmlContent}
34+
</body>
35+
</html>`;
36+
37+
try {
38+
const pdfBytes = await htmlToPdfBuffer(fullHtml);
39+
await downloadFile(pdfBytes, `note-${id}.pdf`, 'application/pdf');
40+
} catch (e) {
41+
console.error('Failed to export PDF:', e);
42+
showMessage('Failed to export PDF. See console for details.', { type: 'alert' });
43+
}
44+
}

src/tools/notes/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface Note {
2+
id?: number;
3+
content: string;
4+
createdAt: number;
5+
updatedAt?: number;
6+
}

0 commit comments

Comments
 (0)