Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ node_modules/
.next/
test-results/
dist/
packages/web/out/
*.log

# OS
Expand Down
153 changes: 96 additions & 57 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@inkah/extension",
"version": "1.1.0",
"version": "2.0.0",
"description": "Chinese & Korean pop-up dictionary browser extension",
"private": true,
"type": "module",
Expand All @@ -18,8 +18,6 @@
"dependencies": {
"classnames": "^2.5.1",
"dexie": "^4.0.0",
"effector": "^23.4.4",
"effector-react": "^23.3.0",
"file-saver": "^2.0.5",
"lucide-react": "^1.7.0",
"react": "^19.2.0",
Expand All @@ -33,6 +31,7 @@
"@wxt-dev/module-react": "^1.1.0",
"autoprefixer": "^10.4.17",
"fake-indexeddb": "^6.0.0",
"happy-dom": "^20.11.1",
"postcss": "^8.5.0",
"sass": "^1.88.0",
"tailwindcss": "^3.4.1",
Expand Down
13 changes: 13 additions & 0 deletions packages/extension/src/data/dict-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ const kedictCache = new Map<string, DictEntry | null>();
const viconCache = new Map<string, DictEntry | null>();
const lemmaCache = new Map<string, LemmaEntry | null>();

/**
* Drop all read-through caches. Must be called after a dictionary import
* completes: lookups served DURING an in-progress import cache misses as
* `null`, and those negative entries would otherwise shadow the freshly
* imported data until the service worker restarts.
*/
export function clearDictionaryCaches(): void {
cedictCache.clear();
kedictCache.clear();
viconCache.clear();
lemmaCache.clear();
}

export async function getCedict(key: string): Promise<DictEntry | undefined> {
if (cedictCache.has(key)) return cedictCache.get(key) ?? undefined;
const entry = await db.cedict.get(key);
Expand Down
27 changes: 24 additions & 3 deletions packages/extension/src/data/import-dictionaries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { db } from './schema';
import { clearDictionaryCaches } from './dict-cache';
import { csvToJson, csvToTags } from '../lib/csv-utils';
import AvailableLanguages from '../lib/available-languages';

Expand Down Expand Up @@ -160,14 +161,28 @@ async function importKoreanDicts(progress: ImportProgress): Promise<void> {
}
}

export async function importDictionariesIfNeeded(): Promise<void> {
// Guard against concurrent runs (onInstalled + service worker startup
// can both trigger an import attempt)
let importInFlight: Promise<void> | null = null;

export function importDictionariesIfNeeded(): Promise<void> {
if (!importInFlight) {
importInFlight = doImport().finally(() => {
importInFlight = null;
});
}
return importInFlight;
}

async function doImport(): Promise<void> {
const progress = await getImportProgress();
if (progress.version >= IMPORT_VERSION) {
console.log('[inkah] Dictionaries already imported');
return;
}

// Import preferred language first, then the other
// Import preferred language first, then the other.
// MV3 service workers can be killed mid-import — per-table progress
// flags let the next startup resume where this run left off.
const langResult = await chrome.storage.local.get(['targetLanguage']);
const preferred: SupportedLanguages = langResult.targetLanguage ?? 'zh';

Expand All @@ -182,5 +197,11 @@ export async function importDictionariesIfNeeded(): Promise<void> {
}

await updateImportProgress({ version: IMPORT_VERSION });

// Lookups served while the import was running cached their misses as
// negative entries — drop them so the imported data is visible now
// rather than after the next service worker restart.
clearDictionaryCaches();

console.log('[inkah] Dictionary import complete!');
}
17 changes: 12 additions & 5 deletions packages/extension/src/entrypoints/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,18 @@ export default defineBackground(() => {
}
});

// Warm the IndexedDB cache on every service worker startup
// (service workers are ephemeral in MV3 — this runs each time one spins up)
warmDictionaryCache().catch((err) => {
console.warn('[inkah] Cache warm failed (dicts may not be imported yet):', err);
});
// On every service worker startup: resume any incomplete dictionary
// import (MV3 workers can be killed mid-import — onInstalled alone
// never retries), then warm the in-memory cache.
importDictionariesIfNeeded()
Comment thread
conoremclaughlin marked this conversation as resolved.
.catch((err) => {
console.error('[inkah] Dictionary import failed:', err);
})
.finally(() => {
warmDictionaryCache().catch((err) => {
console.warn('[inkah] Cache warm failed (dicts may not be imported yet):', err);
});
});

// Message handler for content scripts and popup
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
Expand Down
17 changes: 14 additions & 3 deletions packages/extension/src/entrypoints/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,11 @@ export default defineContentScript({
topPos = anchorBottom + gap;
}

// Final clamp — never let the popup extend past the viewport edges
const actualWidth = popup.offsetWidth;
leftPos = Math.max(8, Math.min(leftPos, viewWidth - actualWidth - 8));
topPos = Math.max(8, Math.min(topPos, viewHeight - popupHeight - 8));

popup.style.left = `${leftPos}px`;
popup.style.top = `${topPos}px`;
}
Expand Down Expand Up @@ -763,7 +768,9 @@ export default defineContentScript({
lockedRangeNode = result.rangeNode;
lockedRangeOffset = result.rangeOffset;
lastPopup = createPopupElement(definitions, result.rect);
document.body.appendChild(lastPopup);
// Append inside the fullscreen element when active — nodes outside
// it don't render while fullscreen
((document.fullscreenElement as HTMLElement | null) ?? document.body).appendChild(lastPopup);
positionPopup(lastPopup);

// Highlight the matched word in the text (old code's setHoverSelection)
Expand Down Expand Up @@ -822,7 +829,9 @@ export default defineContentScript({
showPopup: (definitions: WordDefinitions[], rect: DOMRect) => {
removePopup();
lastPopup = createPopupElement(definitions, rect);
document.body.appendChild(lastPopup);
// Append inside the fullscreen element when active — nodes outside
// it don't render while fullscreen
((document.fullscreenElement as HTMLElement | null) ?? document.body).appendChild(lastPopup);
positionPopup(lastPopup);
},
removePopup,
Expand Down Expand Up @@ -875,7 +884,9 @@ export default defineContentScript({
const rect = range.getBoundingClientRect();
removePopup();
lastPopup = createPopupElement(definitions, rect);
document.body.appendChild(lastPopup);
// Append inside the fullscreen element when active — nodes outside
// it don't render while fullscreen
((document.fullscreenElement as HTMLElement | null) ?? document.body).appendChild(lastPopup);
positionPopup(lastPopup);
}
} catch {}
Expand Down
Loading