diff --git a/src/common/components/DatabaseBusy.tsx b/src/common/components/DatabaseBusy.tsx index 29f0679..bd799f0 100644 --- a/src/common/components/DatabaseBusy.tsx +++ b/src/common/components/DatabaseBusy.tsx @@ -1,7 +1,7 @@ import React from 'react'; import ProgressBar from "./ProgressBar"; -import { DBOperation } from "../../database/database"; -import { Busy, DataSource } from '../../database/dbstatus'; +import { DBOperation } from "../../service-worker/database/indexeddb"; +import { Busy, DataSource } from '../../service-worker/database/dbstatus'; interface DatabaseBusyProps { dbStatus: Busy; diff --git a/src/content-scripts/components/App.tsx b/src/content-scripts/components/App.tsx index e1eae6d..1517ff8 100644 --- a/src/content-scripts/components/App.tsx +++ b/src/content-scripts/components/App.tsx @@ -5,7 +5,7 @@ import { ExtensionContext, ChromeExtensionContext } from "../contexts/ExtensionC import { SegmenterContext } from "../contexts/SegmenterContext"; import { AlertType } from "../../common/components/modal/Alert"; import Modal from "../../common/components/modal/Modal"; -import { DBStatusResult } from "../../database/dbstatus"; +import { DBStatusResult } from "../../service-worker/database/dbstatus"; import VideoContainer from "./VideoContainer"; import { useStorage } from "../../common/hooks/useStorage"; diff --git a/src/content-scripts/components/Subtitle.tsx b/src/content-scripts/components/Subtitle.tsx index 17ae033..951eb9f 100644 --- a/src/content-scripts/components/Subtitle.tsx +++ b/src/content-scripts/components/Subtitle.tsx @@ -2,7 +2,7 @@ import React from "react"; import * as bunsetsu from "bunsetsu"; import Word, { WordIndex } from "./Word"; import { JMdictWord } from "@scriptin/jmdict-simplified-types"; -import { Blocked, Busy, ErrorOccurred, Status, VersionChanged } from "../../database/dbstatus"; +import { Blocked, Busy, ErrorOccurred, Status, VersionChanged } from "../../service-worker/database/dbstatus"; import DatabaseBlocked from "../../common/components/DatabaseBlocked"; import DatabaseBusy from "../../common/components/DatabaseBusy"; import DatabaseError from "../../common/components/DatabaseError"; diff --git a/src/content-scripts/components/Video.tsx b/src/content-scripts/components/Video.tsx index d16e62c..a2aa724 100644 --- a/src/content-scripts/components/Video.tsx +++ b/src/content-scripts/components/Video.tsx @@ -8,7 +8,7 @@ import { toHiragana } from '../../common/lang'; import { SegmenterContext, SegmenterContextI } from '../contexts/SegmenterContext'; import * as bunsetsu from "bunsetsu"; import { sendMessage } from '../util/browser-runtime'; -import { DBStatusResult, Status } from '../../database/dbstatus'; +import { DBStatusResult, Status } from '../../service-worker/database/dbstatus'; import { WordIndex } from './Word'; const NETFLIX_BOTTOM_CONTROLS_CLASS = 'watch-video--bottom-controls-container'; diff --git a/src/content-scripts/components/VideoContainer.tsx b/src/content-scripts/components/VideoContainer.tsx index 56202dd..286f823 100644 --- a/src/content-scripts/components/VideoContainer.tsx +++ b/src/content-scripts/components/VideoContainer.tsx @@ -5,7 +5,7 @@ import { TimedTextTrack, NetflixMetadata, TimedTextSwitch } from "../../common/n import { StorageType } from "../../storage/storage"; import { WEBVTT_FORMAT, querySelectorMutation, ChildMutationType } from "../util/util"; import Video, { WebvttSubtitles } from "./Video"; -import { DBStatusResult } from "../../database/dbstatus"; +import { DBStatusResult } from "../../service-worker/database/dbstatus"; import { useStorage } from "../../common/hooks/useStorage"; import { useExtensionEnabled } from "../../common/hooks/useExtensionEnabled"; diff --git a/src/options/components/PurgeButton.tsx b/src/options/components/PurgeButton.tsx index c22e509..dca4eec 100644 --- a/src/options/components/PurgeButton.tsx +++ b/src/options/components/PurgeButton.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { DBStatusResult, Status } from "../../database/dbstatus"; +import { DBStatusResult, Status } from "../../service-worker/database/dbstatus"; import Spinner from "../../common/components/Spinner"; import { RuntimeMessage, RuntimeEvent } from "../../common/events"; import Modal from "../../common/components/modal/Modal"; diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index dd27751..c0dcd67 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -5,7 +5,7 @@ import DatabaseBlocked from "../common/components/DatabaseBlocked"; import DatabaseBusy from "../common/components/DatabaseBusy"; import DatabaseError from "../common/components/DatabaseError"; import Popup from "./components/Popup"; -import { DBStatusResult, Status } from '../database/dbstatus'; +import { DBStatusResult, Status } from '../service-worker/database/dbstatus'; import { StorageType } from '../storage/storage'; import AppLogo from '../common/components/AppLogo'; import OptionsButton from '../common/components/OptionsButton'; diff --git a/src/service-worker.ts b/src/service-worker.ts deleted file mode 100644 index 3189102..0000000 --- a/src/service-worker.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { IDBUpgradeContext, IDBWrapper, DBOperation, IDBObjectStoreWrapper, DatabaseError, DBErrorType } from "./database/database"; -import { JMDictStore, JMDictStoreUpgrade } from "./database/jmdict"; -import { TatoebaStore, TatoebaStoreUpgrade } from "./database/tatoeba"; -import { KanjiDic2Store, KanjiDic2StoreUpgrade } from "./database/kanjidic2"; -import { CountSentencesMessage, LookupKanjiMessage, LookupSentencesMessage, LookupWordsMessage, PlayAudioMessage, RuntimeEvent, RuntimeMessage, SeekCueMessage, SeekDirection } from "./common/events"; -import * as DBStatusManager from './database/dbstatus' -import * as tabs from './tabs' -import { SessionStorageObject } from "./storage/session-storage"; -import { DataSource } from "./database/dbstatus"; -import { StorageListener, StorageObject, StorageType } from "./storage/storage"; - -const DB_NAME = 'jimakun'; -const DB_VERSION = 3; - -const MOVIE_KEY = 'lastMovieId'; -const ENABLED_KEY = 'enabled'; - -enum Command { - NextCue = 'next-cue', - PrevCue = 'prev-cue', - RepeatCue = 'repeat-cue', - ToggleSubs = 'toggle-subs', -} - -function extractMovieId(url: string): number | undefined { - const regex = new RegExp('netflix.com/watch/([0-9]+)'); - const match = regex.exec(url); - if (!match) { - return undefined; - } - const movie = match[1]; - return Number.isNaN(movie) ? undefined : Number.parseInt(movie); -} - -chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - if (!tab.active) { - return; - } - const url = changeInfo.url ?? tab.url; - if (!url) { - return; - } - const movieId = extractMovieId(url); - if (movieId) { - const storage = new SessionStorageObject(MOVIE_KEY); - storage.set(movieId); - } -}); - -chrome.commands.onCommand.addListener(command => { - switch (command) { - case Command.NextCue: { - const data: SeekCueMessage = { direction: SeekDirection.Next }; - const message: RuntimeMessage = { event: RuntimeEvent.SeekCue, data: data }; - tabs.sendMessageToActive(message); - break; - } - case Command.RepeatCue: { - const data: SeekCueMessage = { direction: SeekDirection.Repeat }; - const message: RuntimeMessage = { event: RuntimeEvent.SeekCue, data: data }; - tabs.sendMessageToActive(message); - break; - } - case Command.PrevCue: { - const data: SeekCueMessage = { direction: SeekDirection.Previous }; - const message: RuntimeMessage = { event: RuntimeEvent.SeekCue, data: data }; - tabs.sendMessageToActive(message); - break; - } - case Command.ToggleSubs: { - const message: RuntimeMessage = { event: RuntimeEvent.ToggleSubs, data: null }; - tabs.sendMessageToActive(message); - break; - } - } -}); - -async function lookupWords(message: LookupWordsMessage, sendResponse: (response?: unknown) => void) { - try { - const dict = await JMDictStore.open(DB_NAME, DB_VERSION, onDBUpgrade, onDBVersionChanged); - const words = await dict.lookupBestMatches(message); - sendResponse(words); - } catch (e) { - sendResponse(undefined); // TODO: better error handling - } -} - -async function lookupKanji(message: LookupKanjiMessage, sendResponse: (response?: unknown) => void) { - try { - const dict = await KanjiDic2Store.open(DB_NAME, DB_VERSION, onDBUpgrade, onDBVersionChanged); - const kanji = await dict.lookup(message); - sendResponse(kanji); - } catch (e) { - sendResponse(undefined); // TODO: better error handling - } -} - -async function countSentences(message: CountSentencesMessage, sendResponse: (response?: unknown) => void) { - try { - const store = await TatoebaStore.open(DB_NAME, DB_VERSION, onDBUpgrade, onDBVersionChanged); - const count = await store.count(message); - sendResponse(count); - } catch (e) { - sendResponse(0); // TODO: better error handling - } -} - -async function lookupSentences(message: LookupSentencesMessage, sendResponse: (response?: unknown) => void) { - try { - const store = await TatoebaStore.open(DB_NAME, DB_VERSION, onDBUpgrade, onDBVersionChanged); - const sentences = await store.lookup(message); - sendResponse(sentences); - } catch (e) { - sendResponse(undefined); // TODO: better error handling - } -} - -function openOptionsPage() { - chrome.runtime.openOptionsPage(() => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error(lastError); - } - }); -} - -function playAudio(message: PlayAudioMessage) { - if (!message.utterance) { - return; - } - chrome.tts.speak(message.utterance, { lang: 'ja' }); -} - -async function purgeDictionaries(sendResponse: (response?: unknown) => void) { - try { - await DBStatusManager.clearStatus(); - await deleteDatabase(); - await initializeDatabase(); - sendResponse(true); - } catch (e) { - handleDatabaseError(e); - sendResponse(false); - } -} - -chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { - const message = request.data; // todo: validate - switch (request.event) { - case RuntimeEvent.LookupWords: - lookupWords(message as LookupWordsMessage, sendResponse); - break; - case RuntimeEvent.LookupKanji: - lookupKanji(message as LookupKanjiMessage, sendResponse); - break; - case RuntimeEvent.CountSentences: - countSentences(message as CountSentencesMessage, sendResponse); - break; - case RuntimeEvent.LookupSentences: - lookupSentences(message as LookupSentencesMessage, sendResponse); - break; - case RuntimeEvent.OpenOptions: - openOptionsPage(); - return false; - case RuntimeEvent.PlayAudio: - playAudio(message as PlayAudioMessage); - return false; - case RuntimeEvent.PurgeDictionaries: - purgeDictionaries(sendResponse); - break; - default: - console.warn("unrecognized request", request); - } - return true; // result will be returned async -} -); - -async function onDBUpgrade(db: IDBUpgradeContext) { - const ctx = db.getContext(); - const upgrades = [ - new JMDictStoreUpgrade(ctx), - new KanjiDic2StoreUpgrade(ctx), - new TatoebaStoreUpgrade(ctx), - ]; - upgrades.map(x => x.apply()); - return db.commit(); -} - -async function onDBVersionChanged() { - await DBStatusManager.setDBStatusVersionChanged(); -} - -async function populateObjectStore(store: IDBObjectStoreWrapper) { - const source = Object.values(DataSource).find(x => x === store.name()); - await DBStatusManager.setDBStatusBusyIndeterminate(DBOperation.Open); - await store.populate(async (operation: DBOperation, value?: number, max?: number) => { - if (value && max) { - return DBStatusManager.setDBStatusBusyDeterminate(operation, value, max, source); - } - return DBStatusManager.setDBStatusBusyIndeterminate(operation, source); - }); -} - -async function populateDatabase(db: IDBWrapper) { - // make sure object stores have the latest data - const objectStores = [ - new JMDictStore(db), - new KanjiDic2Store(db), - new TatoebaStore(db), - ]; - for (const store of objectStores) { - await populateObjectStore(store) - } -} - -async function initializeDatabase() { - await DBStatusManager.setDBStatusBusyIndeterminate(DBOperation.Open); - const db = await IDBWrapper.open(DB_NAME, DB_VERSION, onDBUpgrade, onDBVersionChanged); - await populateDatabase(db); - await DBStatusManager.setDBStatusReady(); -} - -async function deleteDatabase() { - await DBStatusManager.setDBStatusBusyIndeterminate(DBOperation.Delete); - await IDBWrapper.delete(DB_NAME); -} - -function handleDatabaseError(e: unknown) { - if (e instanceof DatabaseError) { - if (e.type === DBErrorType.Blocked) { - DBStatusManager.setDBStatusBlocked(); - } else { - DBStatusManager.setDBStatusError(e); - } - } else if (e instanceof Error) { - DBStatusManager.setDBStatusError(e); - } else { - DBStatusManager.setDBStatusError(new Error('unknown error')); - } -} - -async function toggleExtensionIcon(enabled: boolean) { - const icons = enabled ? { - 16: "icons/icon16.png", - 32: "icons/icon32.png", - 48: "icons/icon48.png", - 128: "icons/icon128.png", - } : { - 16: "icons/icon16-inactive.png", - 32: "icons/icon32-inactive.png", - 48: "icons/icon48-inactive.png", - 128: "icons/icon128-inactive.png", - }; - return chrome.action.setIcon({ - path: icons - }); -} - -function loadAppSettings() { - const storage = new StorageObject(ENABLED_KEY, StorageType.Local); - const onEnabledChanged = new StorageListener(storage, (_, newValue) => toggleExtensionIcon(newValue)); - storage.addOnChangedListener(onEnabledChanged); - storage.get().then(value => { - if (value !== undefined) { - toggleExtensionIcon(value); - } - }); -} - -async function onInstallExtension() { - try { - // session storage can't be accessed from content scripts by default - chrome.storage.session.setAccessLevel({ - accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS' - }); - await DBStatusManager.clearStatus() - await initializeDatabase(); - } catch (e) { - handleDatabaseError(e); - } -} - -function onActivateExtension() { - loadAppSettings(); -} - -chrome.runtime.onStartup.addListener(() => { - console.debug('onStartup event'); - onInstallExtension(); -}); - -chrome.runtime.onInstalled.addListener(() => { - console.debug('onInstalled event'); - onInstallExtension(); -}); - -onActivateExtension(); \ No newline at end of file diff --git a/src/service-worker/commands.ts b/src/service-worker/commands.ts new file mode 100644 index 0000000..1d702fb --- /dev/null +++ b/src/service-worker/commands.ts @@ -0,0 +1,41 @@ +import { SeekCueMessage, SeekDirection, RuntimeMessage, RuntimeEvent } from '../common/events'; +import * as tabs from './tabs' + +enum Command { + NextCue = 'next-cue', + PrevCue = 'prev-cue', + RepeatCue = 'repeat-cue', + ToggleSubs = 'toggle-subs', +} + +function onCommand(command: string) { + switch (command) { + case Command.NextCue: { + const data: SeekCueMessage = { direction: SeekDirection.Next }; + const message: RuntimeMessage = { event: RuntimeEvent.SeekCue, data: data }; + tabs.sendMessageToActive(message); + break; + } + case Command.RepeatCue: { + const data: SeekCueMessage = { direction: SeekDirection.Repeat }; + const message: RuntimeMessage = { event: RuntimeEvent.SeekCue, data: data }; + tabs.sendMessageToActive(message); + break; + } + case Command.PrevCue: { + const data: SeekCueMessage = { direction: SeekDirection.Previous }; + const message: RuntimeMessage = { event: RuntimeEvent.SeekCue, data: data }; + tabs.sendMessageToActive(message); + break; + } + case Command.ToggleSubs: { + const message: RuntimeMessage = { event: RuntimeEvent.ToggleSubs, data: null }; + tabs.sendMessageToActive(message); + break; + } + } +} + +export function registerListeners() { + chrome.commands.onCommand.addListener(onCommand); +} \ No newline at end of file diff --git a/src/service-worker/database.ts b/src/service-worker/database.ts new file mode 100644 index 0000000..3093439 --- /dev/null +++ b/src/service-worker/database.ts @@ -0,0 +1,101 @@ +import { IDBUpgradeContext, IDBObjectStoreWrapper, DBOperation, IDBWrapper, DatabaseError, DBErrorType } from './database/indexeddb'; +import { JMDictStoreUpgrade, JMDictStore } from './database/jmdict'; +import { KanjiDic2StoreUpgrade, KanjiDic2Store } from './database/kanjidic2'; +import { TatoebaStoreUpgrade, TatoebaStore } from './database/tatoeba'; +import * as DBStatusManager from './database/dbstatus'; + +const DB_NAME = 'jimakun'; +const DB_VERSION = 3; + +async function onDBUpgrade(db: IDBUpgradeContext) { + const ctx = db.getContext(); + const upgrades = [ + new JMDictStoreUpgrade(ctx), + new KanjiDic2StoreUpgrade(ctx), + new TatoebaStoreUpgrade(ctx), + ]; + upgrades.map(x => x.apply()); + return db.commit(); +} + +async function onDBVersionChanged() { + await DBStatusManager.setDBStatusVersionChanged(); +} + +async function populateObjectStore(store: IDBObjectStoreWrapper) { + const source = Object.values(DBStatusManager.DataSource).find(x => x === store.name()); + await DBStatusManager.setDBStatusBusyIndeterminate(DBOperation.Open); + await store.populate(async (operation: DBOperation, value?: number, max?: number) => { + if (value && max) { + return DBStatusManager.setDBStatusBusyDeterminate(operation, value, max, source); + } + return DBStatusManager.setDBStatusBusyIndeterminate(operation, source); + }); +} + +async function populateDatabase(db: IDBWrapper) { + // make sure object stores have the latest data + const objectStores = [ + new JMDictStore(db), + new KanjiDic2Store(db), + new TatoebaStore(db), + ]; + for (const store of objectStores) { + await populateObjectStore(store) + } +} + +export async function initializeDatabase() { + try { + await DBStatusManager.clearStatus() + await DBStatusManager.setDBStatusBusyIndeterminate(DBOperation.Open); + const db = await IDBWrapper.open(DB_NAME, DB_VERSION, onDBUpgrade, onDBVersionChanged); + await populateDatabase(db); + await DBStatusManager.setDBStatusReady(); + } catch (e) { + handleDatabaseError(e); + throw e; + } +} + +async function deleteDatabase() { + await DBStatusManager.setDBStatusBusyIndeterminate(DBOperation.Delete); + await IDBWrapper.delete(DB_NAME); +} + +export async function purgeReimport() { + try { + await DBStatusManager.clearStatus(); + await deleteDatabase(); + await initializeDatabase(); + } catch (e) { + handleDatabaseError(e); + throw e; + } +} + +function handleDatabaseError(e: unknown) { + if (e instanceof DatabaseError) { + if (e.type === DBErrorType.Blocked) { + DBStatusManager.setDBStatusBlocked(); + } else { + DBStatusManager.setDBStatusError(e); + } + } else if (e instanceof Error) { + DBStatusManager.setDBStatusError(e); + } else { + DBStatusManager.setDBStatusError(new Error('unknown error')); + } +} + +export async function openJMDict() { + return JMDictStore.open(DB_NAME, DB_VERSION, onDBUpgrade, onDBVersionChanged); +} + +export async function openKanjiDic2() { + return KanjiDic2Store.open(DB_NAME, DB_VERSION, onDBUpgrade, onDBVersionChanged); +} + +export async function openTatoeba() { + return TatoebaStore.open(DB_NAME, DB_VERSION, onDBUpgrade, onDBVersionChanged); +} \ No newline at end of file diff --git a/src/database/data-provider.ts b/src/service-worker/database/data-provider.ts similarity index 92% rename from src/database/data-provider.ts rename to src/service-worker/database/data-provider.ts index c820792..88aa1da 100644 --- a/src/database/data-provider.ts +++ b/src/service-worker/database/data-provider.ts @@ -1,5 +1,5 @@ -import { Checkpoints } from "../common/progress"; -import { ProgressUpdateCallback, DBOperation } from "./database"; +import { Checkpoints } from "../../common/progress"; +import { ProgressUpdateCallback, DBOperation } from "./indexeddb"; type ReadEntriesCallback = (data: Data) => Entry[]; type ParseEntryCallback = (entry: Original) => Parsed; diff --git a/src/database/dbstatus.ts b/src/service-worker/database/dbstatus.ts similarity index 94% rename from src/database/dbstatus.ts rename to src/service-worker/database/dbstatus.ts index d0fd3af..e020bd6 100644 --- a/src/database/dbstatus.ts +++ b/src/service-worker/database/dbstatus.ts @@ -1,6 +1,6 @@ -import { DBOperation } from "./database"; -import { Progress, ProgressType } from "../common/progress"; -import { LocalStorageObject } from "../storage/local-storage"; +import { DBOperation } from "./indexeddb"; +import { Progress, ProgressType } from "../../common/progress"; +import { LocalStorageObject } from "../../storage/local-storage"; const DB_STATUS_KEY = 'lastDBStatusResult' diff --git a/src/database/database.ts b/src/service-worker/database/indexeddb.ts similarity index 99% rename from src/database/database.ts rename to src/service-worker/database/indexeddb.ts index ecde29b..76352a1 100644 --- a/src/database/database.ts +++ b/src/service-worker/database/indexeddb.ts @@ -1,4 +1,4 @@ -import { Checkpoints } from "../common/progress"; +import { Checkpoints } from "../../common/progress"; export enum DBErrorType { Blocked = 'BLOCKED', diff --git a/src/database/jmdict.ts b/src/service-worker/database/jmdict.ts similarity index 98% rename from src/database/jmdict.ts rename to src/service-worker/database/jmdict.ts index 031507a..471a8e3 100644 --- a/src/database/jmdict.ts +++ b/src/service-worker/database/jmdict.ts @@ -1,6 +1,6 @@ import { JMdict, JMdictKana, JMdictKanji, JMdictSense, JMdictWord } from "@scriptin/jmdict-simplified-types"; -import { LookupWordsMessage } from "../common/events"; -import { DBStoreUpgrade, IDBUpgradeContext, IDBWrapper, DBStoreUpgradeContext, ProgressUpdateCallback, IDBObjectStoreWrapper } from "./database"; +import { LookupWordsMessage } from "../../common/events"; +import { DBStoreUpgrade, IDBUpgradeContext, IDBWrapper, DBStoreUpgradeContext, ProgressUpdateCallback, IDBObjectStoreWrapper } from "./indexeddb"; import { JSONDataProvider } from "./data-provider"; const INDEX = { diff --git a/src/database/kanjidic2.ts b/src/service-worker/database/kanjidic2.ts similarity index 96% rename from src/database/kanjidic2.ts rename to src/service-worker/database/kanjidic2.ts index 495e998..4573e52 100644 --- a/src/database/kanjidic2.ts +++ b/src/service-worker/database/kanjidic2.ts @@ -1,6 +1,6 @@ import { Kanjidic2, Kanjidic2Character } from "@scriptin/jmdict-simplified-types"; -import { LookupKanjiMessage } from "../common/events"; -import { IDBWrapper, DBStoreUpgrade, IDBUpgradeContext, DBStoreUpgradeContext, IDBObjectStoreWrapper, ProgressUpdateCallback } from "./database"; +import { LookupKanjiMessage } from "../../common/events"; +import { IDBWrapper, DBStoreUpgrade, IDBUpgradeContext, DBStoreUpgradeContext, IDBObjectStoreWrapper, ProgressUpdateCallback } from "./indexeddb"; import { JSONDataProvider } from "./data-provider"; const OBJECT_STORE = { diff --git a/src/database/tatoeba.ts b/src/service-worker/database/tatoeba.ts similarity index 95% rename from src/database/tatoeba.ts rename to src/service-worker/database/tatoeba.ts index 4f1a1c4..ea9243c 100644 --- a/src/database/tatoeba.ts +++ b/src/service-worker/database/tatoeba.ts @@ -1,7 +1,7 @@ -import { CountSentencesMessage, LookupSentencesMessage, LookupSentencesResult } from "../common/events"; -import { TatoebaSentence, TatoebaWord } from "../common/tatoeba-types"; +import { CountSentencesMessage, LookupSentencesMessage, LookupSentencesResult } from "../../common/events"; +import { TatoebaSentence, TatoebaWord } from "../../common/tatoeba-types"; import { JSONDataProvider } from "./data-provider"; -import { IDBWrapper, DBStoreUpgrade, IDBUpgradeContext, DBStoreUpgradeContext, ProgressUpdateCallback, IDBObjectStoreWrapper } from "./database"; +import { IDBWrapper, DBStoreUpgrade, IDBUpgradeContext, DBStoreUpgradeContext, ProgressUpdateCallback, IDBObjectStoreWrapper } from "./indexeddb"; const INDEX = { name: "keywords", diff --git a/src/service-worker/messages.ts b/src/service-worker/messages.ts new file mode 100644 index 0000000..6680aa1 --- /dev/null +++ b/src/service-worker/messages.ts @@ -0,0 +1,101 @@ +import { LookupWordsMessage, LookupKanjiMessage, CountSentencesMessage, LookupSentencesMessage, PlayAudioMessage, RuntimeEvent, RuntimeMessage } from "../common/events"; +import { openJMDict, openKanjiDic2, openTatoeba, purgeReimport } from "./database"; + +async function lookupWords(message: LookupWordsMessage, sendResponse: (response?: unknown) => void) { + try { + const dict = await openJMDict(); + const words = await dict.lookupBestMatches(message); + sendResponse(words); + } catch (e) { + sendResponse(undefined); // TODO: better error handling + } +} + +async function lookupKanji(message: LookupKanjiMessage, sendResponse: (response?: unknown) => void) { + try { + const dict = await openKanjiDic2(); + const kanji = await dict.lookup(message); + sendResponse(kanji); + } catch (e) { + sendResponse(undefined); // TODO: better error handling + } +} + +async function countSentences(message: CountSentencesMessage, sendResponse: (response?: unknown) => void) { + try { + const store = await openTatoeba(); + const count = await store.count(message); + sendResponse(count); + } catch (e) { + sendResponse(0); // TODO: better error handling + } +} + +async function lookupSentences(message: LookupSentencesMessage, sendResponse: (response?: unknown) => void) { + try { + const store = await openTatoeba(); + const sentences = await store.lookup(message); + sendResponse(sentences); + } catch (e) { + sendResponse(undefined); // TODO: better error handling + } +} + +function openOptionsPage() { + chrome.runtime.openOptionsPage(() => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error(lastError); + } + }); +} + +function playAudio(message: PlayAudioMessage) { + if (!message.utterance) { + return; + } + chrome.tts.speak(message.utterance, { lang: 'ja' }); +} + +async function purgeDictionaries(sendResponse: (response?: unknown) => void) { + try { + await purgeReimport(); + sendResponse(true); + } catch (e) { + sendResponse(false); + } +} + +function onMessage(request: RuntimeMessage, _sender: chrome.runtime.MessageSender, sendResponse: (response?: unknown) => void) { + const message = request.data; // todo: validate + switch (request.event) { + case RuntimeEvent.LookupWords: + lookupWords(message as LookupWordsMessage, sendResponse); + break; + case RuntimeEvent.LookupKanji: + lookupKanji(message as LookupKanjiMessage, sendResponse); + break; + case RuntimeEvent.CountSentences: + countSentences(message as CountSentencesMessage, sendResponse); + break; + case RuntimeEvent.LookupSentences: + lookupSentences(message as LookupSentencesMessage, sendResponse); + break; + case RuntimeEvent.OpenOptions: + openOptionsPage(); + return false; + case RuntimeEvent.PlayAudio: + playAudio(message as PlayAudioMessage); + return false; + case RuntimeEvent.PurgeDictionaries: + purgeDictionaries(sendResponse); + break; + default: + console.warn("unrecognized request", request); + } + return true; // result will be returned async +} + +export function registerListeners() { + chrome.runtime.onMessage.addListener(onMessage); +} \ No newline at end of file diff --git a/src/service-worker/service-worker.ts b/src/service-worker/service-worker.ts new file mode 100644 index 0000000..cc77d80 --- /dev/null +++ b/src/service-worker/service-worker.ts @@ -0,0 +1,38 @@ +import * as commands from './commands' +import * as messages from './messages' +import * as settings from './settings' +import * as tabs from './tabs' +import * as database from './database' + +async function onInstallExtension() { + try { + // session storage can't be accessed from content scripts by default + chrome.storage.session.setAccessLevel({ + accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS' + }); + await database.initializeDatabase(); + } catch (e) { + console.error(e); + } +} + +function onExecuteServiceWorker() { + chrome.runtime.onStartup.addListener(onStartup); + chrome.runtime.onInstalled.addListener(onInstalled); + tabs.registerListeners(); + messages.registerListeners(); + commands.registerListeners(); + settings.loadAppSettings(); +} + +function onStartup() { + console.debug('onStartup event'); + onInstallExtension(); +} + +function onInstalled() { + console.debug('onInstalled event'); + onInstallExtension(); +} + +onExecuteServiceWorker(); \ No newline at end of file diff --git a/src/service-worker/settings.ts b/src/service-worker/settings.ts new file mode 100644 index 0000000..e78004d --- /dev/null +++ b/src/service-worker/settings.ts @@ -0,0 +1,31 @@ +import { StorageObject, StorageType, StorageListener } from "../storage/storage"; + +const ENABLED_KEY = 'enabled'; + +async function toggleExtensionIcon(enabled: boolean) { + const icons = enabled ? { + 16: "icons/icon16.png", + 32: "icons/icon32.png", + 48: "icons/icon48.png", + 128: "icons/icon128.png", + } : { + 16: "icons/icon16-inactive.png", + 32: "icons/icon32-inactive.png", + 48: "icons/icon48-inactive.png", + 128: "icons/icon128-inactive.png", + }; + return chrome.action.setIcon({ + path: icons + }); +} + +export function loadAppSettings() { + const storage = new StorageObject(ENABLED_KEY, StorageType.Local); + const onEnabledChanged = new StorageListener(storage, (_, newValue) => toggleExtensionIcon(newValue)); + storage.addOnChangedListener(onEnabledChanged); + storage.get().then(value => { + if (value !== undefined) { + toggleExtensionIcon(value); + } + }); +} \ No newline at end of file diff --git a/src/tabs.ts b/src/service-worker/tabs.ts similarity index 56% rename from src/tabs.ts rename to src/service-worker/tabs.ts index d8b726c..9f164f3 100644 --- a/src/tabs.ts +++ b/src/service-worker/tabs.ts @@ -1,4 +1,7 @@ -import { RuntimeMessage } from "./common/events"; +import { RuntimeMessage } from "../common/events"; +import { SessionStorageObject } from "../storage/session-storage"; + +const MOVIE_KEY = 'lastMovieId'; export async function sendMessageToAll(message: RuntimeMessage) { try { @@ -34,4 +37,33 @@ export function sendMessageTo(tabId: number, message: RuntimeMessage) { chrome.tabs.sendMessage(tabId, message, () => { console.warn(`message not received by tab ${tabId}`, chrome.runtime.lastError); }); +} + +function extractMovieId(url: string): number | undefined { + const regex = new RegExp('netflix.com/watch/([0-9]+)'); + const match = regex.exec(url); + if (!match) { + return undefined; + } + const movie = match[1]; + return Number.isNaN(movie) ? undefined : Number.parseInt(movie); +} + +function onTabsUpdated(tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) { + if (!tab.active) { + return; + } + const url = changeInfo.url ?? tab.url; + if (!url) { + return; + } + const movieId = extractMovieId(url); + if (movieId) { + const storage = new SessionStorageObject(MOVIE_KEY); + storage.set(movieId); + } +} + +export function registerListeners() { + chrome.tabs.onUpdated.addListener(onTabsUpdated); } \ No newline at end of file diff --git a/webpack.config.ts b/webpack.config.ts index 8de3d1d..02ccfbd 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -54,7 +54,7 @@ module.exports = ( options: "./src/options/options.tsx", content: "./src/content-scripts/content.tsx", interceptor: "./src/content-scripts/interceptor.ts", - "service-worker": "./src/service-worker.ts", + "service-worker": "./src/service-worker/service-worker.ts", }, output: { path: outputDir,