/
githubmirror
/
obsidian-clipper
Обзор
Документация
Войти
/
githubmirror
/
obsidian-clipper
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
0.11.2
src/background.ts
396 строк
12 KB
Steph Ango
Separate reader script (#407)
19 мар 2025, 22:06
Не верифицирован
19 мар 2025, 22:06
e35cc70
Код
Авторство
О чём код?
import browser from './utils/browser-polyfill'; import { ensureContentScriptLoaded } from './utils/content-script-utils'; import { detectBrowser } from './utils/browser-detection'; import { updateCurrentActiveTab, isValidUrl, isBlankPage } from './utils/active-tab-manager'; import { TextHighlightData } from './utils/highlighter'; import { debounce } from './utils/debounce'; let sidePanelOpenWindows: Set<number> = new Set(); let isHighlighterMode = false; let hasHighlights = false; let isContextMenuCreating = false; let popupPorts: { [tabId: number]: browser.Runtime.Port } = {}; // Check if a popup is open for a given tab function isPopupOpen(tabId: number): boolean { return popupPorts.hasOwnProperty(tabId); } browser.runtime.onConnect.addListener((port) => { if (port.name === 'popup') { const tabId = port.sender?.tab?.id; if (tabId) { popupPorts[tabId] = port; port.onDisconnect.addListener(() => { delete popupPorts[tabId]; }); } } }); async function sendMessageToPopup(tabId: number, message: any): Promise<void> { if (isPopupOpen(tabId)) { try { await popupPorts[tabId].postMessage(message); } catch (error) { console.warn(`Error sending message to popup for tab ${tabId}:`, error); } } } browser.action.onClicked.addListener((tab) => { if (tab.id) { browser.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] }); } }); browser.runtime.onMessage.addListener((request: unknown, sender: browser.Runtime.MessageSender, sendResponse: (response?: any) => void): true | undefined => { if (typeof request === 'object' && request !== null) { const typedRequest = request as { action: string; isActive?: boolean; hasHighlights?: boolean; tabId?: number }; if (typedRequest.action === "extractContent" && sender.tab && sender.tab.id) { browser.tabs.sendMessage(sender.tab.id, request).then(sendResponse); return true; } if (typedRequest.action === "ensureContentScriptLoaded" && typedRequest.tabId) { ensureContentScriptLoaded(typedRequest.tabId).then(sendResponse); return true; } if (typedRequest.action === "sidePanelOpened") { if (sender.tab && sender.tab.windowId) { sidePanelOpenWindows.add(sender.tab.windowId); updateCurrentActiveTab(sender.tab.windowId); } } if (typedRequest.action === "sidePanelClosed") { if (sender.tab && sender.tab.windowId) { sidePanelOpenWindows.delete(sender.tab.windowId); } } if (typedRequest.action === "highlighterModeChanged" && sender.tab && typedRequest.isActive !== undefined) { isHighlighterMode = typedRequest.isActive; if (sender.tab.id) { sendMessageToPopup(sender.tab.id, { action: "updatePopupHighlighterUI", isActive: isHighlighterMode }); } debouncedUpdateContextMenu(sender.tab.id!); } if (typedRequest.action === "highlightsCleared" && sender.tab) { hasHighlights = false; debouncedUpdateContextMenu(sender.tab.id!); } if (typedRequest.action === "updateHasHighlights" && sender.tab && typedRequest.hasHighlights !== undefined) { hasHighlights = typedRequest.hasHighlights; debouncedUpdateContextMenu(sender.tab.id!); } if (typedRequest.action === "getHighlighterMode") { const result = browser.storage.local.get('isHighlighterMode'); sendResponse({ isActive: result }); } if (typedRequest.action === "toggleHighlighterMode" && typedRequest.tabId) { toggleHighlighterMode(typedRequest.tabId); sendResponse({ success: true }); } if (typedRequest.action === "openPopup") { browser.action.openPopup() .then(() => { sendResponse({ success: true }); }) .catch((error: unknown) => { console.error('Error opening popup in background script:', error); sendResponse({ success: false, error: error instanceof Error ? error.message : String(error) }); }); return true; } if (typedRequest.action === "toggleReaderMode" && typedRequest.tabId) { injectReaderScript(typedRequest.tabId).then(() => { browser.tabs.sendMessage(typedRequest.tabId!, { action: "toggleReaderMode" }) .then(sendResponse); }); return true; } // For other actions that use sendResponse if (typedRequest.action === "extractContent" || typedRequest.action === "ensureContentScriptLoaded" || typedRequest.action === "getHighlighterMode" || typedRequest.action === "toggleHighlighterMode") { return true; } } return undefined; }); browser.commands.onCommand.addListener(async (command, tab) => { if (command === 'quick_clip') { browser.tabs.query({active: true, currentWindow: true}).then((tabs) => { if (tabs[0]?.id) { browser.action.openPopup(); setTimeout(() => { browser.runtime.sendMessage({action: "triggerQuickClip"}) .catch(error => console.error("Failed to send quick clip message:", error)); }, 500); } }); } if (command === "toggle_highlighter" && tab && tab.id) { await ensureContentScriptLoaded(tab.id); toggleHighlighterMode(tab.id); } if (command === "copy_to_clipboard" && tab && tab.id) { await browser.tabs.sendMessage(tab.id, { action: "copyToClipboard" }); } if (command === "toggle_reader" && tab && tab.id) { await ensureContentScriptLoaded(tab.id); await injectReaderScript(tab.id); await browser.tabs.sendMessage(tab.id, { action: "toggleReaderMode" }); } }); const debouncedUpdateContextMenu = debounce(async (tabId: number) => { if (isContextMenuCreating) { return; } isContextMenuCreating = true; try { await browser.contextMenus.removeAll(); const menuItems: { id: string; title: string; contexts: browser.Menus.ContextType[]; }[] = [ { id: "open-obsidian-clipper", title: "Save this page", contexts: ["page", "selection", "image", "video", "audio"] }, // { // id: "toggle-reader", // title: "Reading view", // contexts: ["page", "selection"] // }, { id: isHighlighterMode ? "exit-highlighter" : "enter-highlighter", title: isHighlighterMode ? "Exit highlighter" : "Highligh this page", contexts: ["page","image", "video", "audio"] }, { id: "highlight-selection", title: "Add to highlights", contexts: ["selection"] }, { id: "highlight-element", title: "Add to highlights", contexts: ["image", "video", "audio"] } ]; const browserType = await detectBrowser(); if (browserType === 'chrome') { menuItems.push({ id: 'open-side-panel', title: 'Open side panel', contexts: ["page", "selection"] }); } for (const item of menuItems) { await browser.contextMenus.create(item); } } catch (error) { console.error('Error updating context menu:', error); } finally { isContextMenuCreating = false; } }, 100); // 100ms debounce time browser.contextMenus.onClicked.addListener(async (info, tab) => { if (info.menuItemId === "open-obsidian-clipper") { browser.action.openPopup(); } else if (info.menuItemId === "enter-highlighter" && tab && tab.id) { await setHighlighterMode(tab.id, true); } else if (info.menuItemId === "exit-highlighter" && tab && tab.id) { await setHighlighterMode(tab.id, false); } else if (info.menuItemId === "highlight-selection" && tab && tab.id) { await highlightSelection(tab.id, info); } else if (info.menuItemId === "highlight-element" && tab && tab.id) { await highlightElement(tab.id, info); // } else if (info.menuItemId === "toggle-reader" && tab && tab.id) { // await ensureContentScriptLoaded(tab.id); // await browser.tabs.sendMessage(tab.id, { action: "toggleReaderMode" }); } else if (info.menuItemId === 'open-side-panel' && tab && tab.id && tab.windowId) { chrome.sidePanel.open({ tabId: tab.id }); sidePanelOpenWindows.add(tab.windowId); await ensureContentScriptLoaded(tab.id); } }); browser.runtime.onInstalled.addListener(() => { debouncedUpdateContextMenu(-1); // Use a dummy tabId for initial creation }); async function isSidePanelOpen(windowId: number): Promise<boolean> { return sidePanelOpenWindows.has(windowId); } async function setupTabListeners() { const browserType = await detectBrowser(); if (['chrome', 'brave', 'edge'].includes(browserType)) { browser.tabs.onActivated.addListener(handleTabChange); browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete') { handleTabChange({ tabId, windowId: tab.windowId }); } }); } } const debouncedPaintHighlights = debounce(async (tabId: number) => { await setHighlighterMode(tabId, false); await paintHighlights(tabId); }, 250); async function handleTabChange(activeInfo: { tabId: number; windowId?: number }) { if (activeInfo.windowId && await isSidePanelOpen(activeInfo.windowId)) { updateCurrentActiveTab(activeInfo.windowId); await debouncedPaintHighlights(activeInfo.tabId); } } async function paintHighlights(tabId: number) { try { const tab = await browser.tabs.get(tabId); if (!tab || !tab.url || !isValidUrl(tab.url) || isBlankPage(tab.url)) { return; } await ensureContentScriptLoaded(tabId); await browser.tabs.sendMessage(tabId, { action: "paintHighlights" }); } catch (error) { console.error('Error painting highlights:', error); } } async function setHighlighterMode(tabId: number, activate: boolean) { try { // First, check if the tab exists const tab = await browser.tabs.get(tabId); if (!tab || !tab.url) { return; } // Check if the URL is valid and not a blank page if (!isValidUrl(tab.url) || isBlankPage(tab.url)) { return; } // Then, ensure the content script is loaded await ensureContentScriptLoaded(tabId); // Now try to send the message isHighlighterMode = activate; await browser.storage.local.set({ isHighlighterMode: activate }); await browser.tabs.sendMessage(tabId, { action: "setHighlighterMode", isActive: activate }); debouncedUpdateContextMenu(tabId); await sendMessageToPopup(tabId, { action: "updatePopupHighlighterUI", isActive: activate }); } catch (error) { console.error('Error setting highlighter mode:', error); // If there's an error, assume highlighter mode should be off isHighlighterMode = false; await browser.storage.local.set({ isHighlighterMode: false }); debouncedUpdateContextMenu(tabId); await sendMessageToPopup(tabId, { action: "updatePopupHighlighterUI", isActive: false }); } } async function toggleHighlighterMode(tabId: number) { try { const result = await browser.storage.local.get('isHighlighterMode'); const currentMode = result.isHighlighterMode || false; const newMode = !currentMode; await browser.storage.local.set({ isHighlighterMode: newMode }); await browser.tabs.sendMessage(tabId, { action: "setHighlighterMode", isActive: newMode }); debouncedUpdateContextMenu(tabId); await sendMessageToPopup(tabId, { action: "updatePopupHighlighterUI", isActive: newMode }); } catch (error) { console.error('Error toggling highlighter mode:', error); } } async function highlightSelection(tabId: number, info: browser.Menus.OnClickData) { isHighlighterMode = true; const highlightData: Partial<TextHighlightData> = { id: Date.now().toString(), type: 'text', content: info.selectionText || '', }; await browser.tabs.sendMessage(tabId, { action: "highlightSelection", isActive: isHighlighterMode, highlightData, }); hasHighlights = true; debouncedUpdateContextMenu(tabId); } async function highlightElement(tabId: number, info: browser.Menus.OnClickData) { isHighlighterMode = true; await browser.tabs.sendMessage(tabId, { action: "highlightElement", isActive: isHighlighterMode, targetElementInfo: { mediaType: info.mediaType === 'image' ? 'img' : info.mediaType, srcUrl: info.srcUrl, pageUrl: info.pageUrl } }); hasHighlights = true; debouncedUpdateContextMenu(tabId); } async function injectReaderScript(tabId: number) { try { await browser.scripting.insertCSS({ target: { tabId }, files: ['reader.css'] }); await browser.scripting.executeScript({ target: { tabId }, files: ['reader-script.js'] }); return true; } catch (error) { console.error('Error injecting reader script:', error); return false; } } // Initialize the tab listeners setupTabListeners(); // Initialize the global highlighter state when the extension starts browser.storage.local.get('isHighlighterMode').then((result: { isHighlighterMode?: boolean }) => { isHighlighterMode = result.isHighlighterMode ?? false; });