aboutsummaryrefslogtreecommitdiffstats
path: root/apps/browser-extension/src/background
diff options
context:
space:
mode:
Diffstat (limited to 'apps/browser-extension/src/background')
-rw-r--r--apps/browser-extension/src/background/background.ts481
1 files changed, 357 insertions, 124 deletions
diff --git a/apps/browser-extension/src/background/background.ts b/apps/browser-extension/src/background/background.ts
index 3d3c1032..04089cc8 100644
--- a/apps/browser-extension/src/background/background.ts
+++ b/apps/browser-extension/src/background/background.ts
@@ -1,124 +1,357 @@
-import {
- BookmarkTypes,
- ZNewBookmarkRequest,
-} from "@karakeep/shared/types/bookmarks.ts";
-
-import {
- getPluginSettings,
- Settings,
- subscribeToSettingsChanges,
-} from "../utils/settings.ts";
-import { NEW_BOOKMARK_REQUEST_KEY_NAME } from "./protocol.ts";
-
-const OPEN_KARAKEEP_ID = "open-karakeep";
-const ADD_LINK_TO_KARAKEEP_ID = "add-link";
-
-function checkSettingsState(settings: Settings) {
- if (settings?.address) {
- registerContextMenus();
- } else {
- removeContextMenus();
- }
-}
-
-function removeContextMenus() {
- chrome.contextMenus.remove(OPEN_KARAKEEP_ID);
- chrome.contextMenus.remove(ADD_LINK_TO_KARAKEEP_ID);
-}
-
-/**
- * Registers
- * * a context menu button to open a tab with the currently configured karakeep instance
- * * a context menu button to add a link to karakeep without loading the page
- */
-function registerContextMenus() {
- chrome.contextMenus.create({
- id: OPEN_KARAKEEP_ID,
- title: "Open Karakeep",
- contexts: ["action"],
- });
- chrome.contextMenus.create({
- id: ADD_LINK_TO_KARAKEEP_ID,
- title: "Add to Karakeep",
- contexts: ["link", "page", "selection", "image"],
- });
-}
-
-/**
- * Reads the current settings and opens a new tab with karakeep
- * @param info the information about the click in the context menu
- */
-async function handleContextMenuClick(info: chrome.contextMenus.OnClickData) {
- const { menuItemId, selectionText, srcUrl, linkUrl, pageUrl } = info;
- if (menuItemId === OPEN_KARAKEEP_ID) {
- getPluginSettings().then((settings: Settings) => {
- chrome.tabs.create({ url: settings.address, active: true });
- });
- } else if (menuItemId === ADD_LINK_TO_KARAKEEP_ID) {
- addLinkToKarakeep({ selectionText, srcUrl, linkUrl, pageUrl });
-
- // NOTE: Firefox only allows opening context menus if it's triggered by a user action.
- // awaiting on any promise before calling this function will lose the "user action" context.
- await chrome.action.openPopup();
- }
-}
-
-function addLinkToKarakeep({
- selectionText,
- srcUrl,
- linkUrl,
- pageUrl,
-}: {
- selectionText?: string;
- srcUrl?: string;
- linkUrl?: string;
- pageUrl?: string;
-}) {
- let newBookmark: ZNewBookmarkRequest | null = null;
- if (selectionText) {
- newBookmark = {
- type: BookmarkTypes.TEXT,
- text: selectionText,
- sourceUrl: pageUrl,
- };
- } else if (srcUrl ?? linkUrl ?? pageUrl) {
- newBookmark = {
- type: BookmarkTypes.LINK,
- url: srcUrl ?? linkUrl ?? pageUrl ?? "",
- };
- }
- if (newBookmark) {
- chrome.storage.session.set({
- [NEW_BOOKMARK_REQUEST_KEY_NAME]: newBookmark,
- });
- }
-}
-
-getPluginSettings().then((settings: Settings) => {
- checkSettingsState(settings);
-});
-
-subscribeToSettingsChanges((settings) => {
- checkSettingsState(settings);
-});
-
-// eslint-disable-next-line @typescript-eslint/no-misused-promises -- Manifest V3 allows async functions for all callbacks
-chrome.contextMenus.onClicked.addListener(handleContextMenuClick);
-
-function handleCommand(command: string, tab: chrome.tabs.Tab) {
- if (command === ADD_LINK_TO_KARAKEEP_ID) {
- addLinkToKarakeep({
- selectionText: undefined,
- srcUrl: undefined,
- linkUrl: undefined,
- pageUrl: tab?.url,
- });
-
- // now try to open the popup
- chrome.action.openPopup();
- } else {
- console.warn(`Received unknown command: ${command}`);
- }
-}
-
-chrome.commands.onCommand.addListener(handleCommand);
+import {
+ BookmarkTypes,
+ ZNewBookmarkRequest,
+} from "@karakeep/shared/types/bookmarks";
+
+import { clearBadgeStatus, getBadgeStatus } from "../utils/badgeCache";
+import {
+ getPluginSettings,
+ Settings,
+ subscribeToSettingsChanges,
+} from "../utils/settings";
+import { getApiClient, initializeClients } from "../utils/trpc";
+import { MessageType } from "../utils/type";
+import { isHttpUrl } from "../utils/url";
+import { NEW_BOOKMARK_REQUEST_KEY_NAME } from "./protocol";
+
+const OPEN_KARAKEEP_ID = "open-karakeep";
+const ADD_LINK_TO_KARAKEEP_ID = "add-link";
+const CLEAR_CURRENT_CACHE_ID = "clear-current-cache";
+const CLEAR_ALL_CACHE_ID = "clear-all-cache";
+const SEPARATOR_ID = "separator-1";
+const VIEW_PAGE_IN_KARAKEEP = "view-page-in-karakeep";
+
+/**
+ * Check the current settings state and register or remove context menus accordingly.
+ * @param settings The current plugin settings.
+ */
+async function checkSettingsState(settings: Settings) {
+ await initializeClients();
+ if (settings?.address && settings?.apiKey) {
+ registerContextMenus(settings);
+ } else {
+ removeContextMenus();
+ await clearAllCache();
+ }
+}
+
+/**
+ * Remove context menus from the browser.
+ */
+function removeContextMenus() {
+ try {
+ chrome.contextMenus.removeAll();
+ } catch (error) {
+ console.error("Failed to remove context menus:", error);
+ }
+}
+
+/**
+ * Register context menus in the browser.
+ * * A context menu button to open a tab with the currently configured karakeep instance.
+ * * * If the "show count badge" setting is enabled, add context menu buttons to clear the cache for the current page or all pages.
+ * * A context menu button to add a link to karakeep without loading the page.
+ * @param settings The current plugin settings.
+ */
+function registerContextMenus(settings: Settings) {
+ removeContextMenus();
+ chrome.contextMenus.create({
+ id: OPEN_KARAKEEP_ID,
+ title: "Open Karakeep",
+ contexts: ["action"],
+ });
+
+ chrome.contextMenus.create({
+ id: ADD_LINK_TO_KARAKEEP_ID,
+ title: "Add to Karakeep",
+ contexts: ["link", "page", "selection", "image"],
+ });
+
+ if (settings?.showCountBadge) {
+ chrome.contextMenus.create({
+ id: VIEW_PAGE_IN_KARAKEEP,
+ title: "View this page in Karakeep",
+ contexts: ["action", "page"],
+ });
+ if (settings?.useBadgeCache) {
+ // Add separator
+ chrome.contextMenus.create({
+ id: SEPARATOR_ID,
+ type: "separator",
+ contexts: ["action"],
+ });
+
+ chrome.contextMenus.create({
+ id: CLEAR_CURRENT_CACHE_ID,
+ title: "Clear Current Page Cache",
+ contexts: ["action"],
+ });
+
+ chrome.contextMenus.create({
+ id: CLEAR_ALL_CACHE_ID,
+ title: "Clear All Cache",
+ contexts: ["action"],
+ });
+ }
+ }
+}
+
+/**
+ * Handle context menu clicks by opening a new tab with karakeep or adding a link to karakeep.
+ * @param info Information about the context menu click event.
+ * @param tab The current tab.
+ */
+async function handleContextMenuClick(
+ info: chrome.contextMenus.OnClickData,
+ tab?: chrome.tabs.Tab,
+) {
+ const { menuItemId, selectionText, srcUrl, linkUrl, pageUrl } = info;
+ if (menuItemId === OPEN_KARAKEEP_ID) {
+ getPluginSettings().then((settings: Settings) => {
+ chrome.tabs.create({ url: settings.address, active: true });
+ });
+ } else if (menuItemId === CLEAR_CURRENT_CACHE_ID) {
+ await clearCurrentPageCache();
+ } else if (menuItemId === CLEAR_ALL_CACHE_ID) {
+ await clearAllCache();
+ } else if (menuItemId === ADD_LINK_TO_KARAKEEP_ID) {
+ addLinkToKarakeep({
+ selectionText,
+ srcUrl,
+ linkUrl,
+ pageUrl,
+ title: tab?.title,
+ });
+
+ // NOTE: Firefox only allows opening context menus if it's triggered by a user action.
+ // awaiting on any promise before calling this function will lose the "user action" context.
+ await chrome.action.openPopup();
+ } else if (menuItemId === VIEW_PAGE_IN_KARAKEEP) {
+ if (tab) {
+ await searchCurrentUrl(tab.url);
+ }
+ }
+}
+
+/**
+ * Add a link to karakeep based on the provided information.
+ * @param options An object containing information about the link to add.
+ */
+function addLinkToKarakeep({
+ selectionText,
+ srcUrl,
+ linkUrl,
+ pageUrl,
+ title,
+}: {
+ selectionText?: string;
+ srcUrl?: string;
+ linkUrl?: string;
+ pageUrl?: string;
+ title?: string;
+}) {
+ let newBookmark: ZNewBookmarkRequest | null = null;
+ if (selectionText) {
+ newBookmark = {
+ type: BookmarkTypes.TEXT,
+ text: selectionText,
+ sourceUrl: pageUrl,
+ source: "extension",
+ };
+ } else {
+ const finalUrl = srcUrl ?? linkUrl ?? pageUrl;
+
+ if (finalUrl && isHttpUrl(finalUrl)) {
+ newBookmark = {
+ type: BookmarkTypes.LINK,
+ url: finalUrl,
+ source: "extension",
+ title,
+ };
+ } else {
+ console.warn("Invalid URL, bookmark not created:", finalUrl);
+ }
+ }
+ if (newBookmark) {
+ chrome.storage.session.set({
+ [NEW_BOOKMARK_REQUEST_KEY_NAME]: newBookmark,
+ });
+ }
+}
+
+/**
+ * Search current URL and open appropriate page.
+ */
+async function searchCurrentUrl(tabUrl?: string) {
+ try {
+ if (!tabUrl || !isHttpUrl(tabUrl)) {
+ console.warn("Invalid URL, cannot search:", tabUrl);
+ return;
+ }
+ console.log("Searching bookmarks for URL:", tabUrl);
+
+ const settings = await getPluginSettings();
+ const serverAddress = settings.address;
+
+ const matchedBookmarkId = await getBadgeStatus(tabUrl);
+ let targetUrl: string;
+ if (matchedBookmarkId) {
+ // Found exact match, open bookmark details page
+ targetUrl = `${serverAddress}/dashboard/preview/${matchedBookmarkId}`;
+ console.log("Opening bookmark details page:", targetUrl);
+ } else {
+ // No exact match, open search results page
+ const searchQuery = encodeURIComponent(`url:${tabUrl}`);
+ targetUrl = `${serverAddress}/dashboard/search?q=${searchQuery}`;
+ console.log("Opening search results page:", targetUrl);
+ }
+ await chrome.tabs.create({ url: targetUrl, active: true });
+ } catch (error) {
+ console.error("Failed to search current URL:", error);
+ }
+}
+
+/**
+ * Clear badge cache for the current active page.
+ */
+async function clearCurrentPageCache() {
+ try {
+ // Get the active tab
+ const [activeTab] = await chrome.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+
+ if (activeTab.url && activeTab.id) {
+ console.log("Clearing cache for current page:", activeTab.url);
+ await clearBadgeStatus(activeTab.url);
+
+ // Refresh the badge for the current tab
+ await checkAndUpdateIcon(activeTab.id);
+ }
+ } catch (error) {
+ console.error("Failed to clear current page cache:", error);
+ }
+}
+
+/**
+ * Clear all badge cache and refresh badges for all active tabs.
+ */
+async function clearAllCache() {
+ try {
+ console.log("Clearing all badge cache");
+ await clearBadgeStatus();
+ } catch (error) {
+ console.error("Failed to clear all cache:", error);
+ }
+}
+
+getPluginSettings().then(async (settings: Settings) => {
+ await checkSettingsState(settings);
+});
+
+subscribeToSettingsChanges(async (settings) => {
+ await checkSettingsState(settings);
+});
+
+// eslint-disable-next-line @typescript-eslint/no-misused-promises -- Manifest V3 allows async functions for all callbacks
+chrome.contextMenus.onClicked.addListener(handleContextMenuClick);
+
+/**
+ * Handle command events, such as adding a link to karakeep.
+ * @param command The command to handle.
+ * @param tab The current tab.
+ */
+function handleCommand(command: string, tab: chrome.tabs.Tab) {
+ if (command === ADD_LINK_TO_KARAKEEP_ID) {
+ addLinkToKarakeep({
+ selectionText: undefined,
+ srcUrl: undefined,
+ linkUrl: undefined,
+ pageUrl: tab?.url,
+ });
+
+ // now try to open the popup
+ chrome.action.openPopup();
+ } else {
+ console.warn(`Received unknown command: ${command}`);
+ }
+}
+
+chrome.commands.onCommand.addListener(handleCommand);
+
+/**
+ * Set the badge text and color based on the provided information.
+ * @param badgeStatus
+ * @param tabId The ID of the tab to update.
+ */
+export async function setBadge(badgeStatus: string | null, tabId?: number) {
+ if (!tabId) return;
+
+ if (badgeStatus) {
+ return await Promise.all([
+ chrome.action.setBadgeText({ tabId, text: ` ` }),
+ chrome.action.setBadgeBackgroundColor({
+ tabId,
+ color: "#4CAF50",
+ }),
+ ]);
+ } else {
+ await chrome.action.setBadgeText({ tabId, text: `` });
+ }
+}
+
+/**
+ * Check and update the badge icon for a given tab ID.
+ * @param tabId The ID of the tab to update.
+ */
+async function checkAndUpdateIcon(tabId: number) {
+ const tabInfo = await chrome.tabs.get(tabId);
+ const { showCountBadge } = await getPluginSettings();
+ const api = await getApiClient();
+ if (
+ !api ||
+ !showCountBadge ||
+ !tabInfo.url ||
+ !isHttpUrl(tabInfo.url) ||
+ tabInfo.status !== "complete"
+ ) {
+ await chrome.action.setBadgeText({ tabId, text: "" });
+ return;
+ }
+ console.log("Tab activated", tabId, tabInfo);
+
+ try {
+ const status = await getBadgeStatus(tabInfo.url);
+ await setBadge(status, tabId);
+ } catch (error) {
+ console.error("Archive check failed:", error);
+ await setBadge(null, tabId);
+ }
+}
+
+chrome.tabs.onActivated.addListener(async (tabActiveInfo) => {
+ await checkAndUpdateIcon(tabActiveInfo.tabId);
+});
+
+chrome.tabs.onUpdated.addListener(async (tabId) => {
+ await checkAndUpdateIcon(tabId);
+});
+
+// Listen for REFRESH_BADGE messages from popup and update badge accordingly
+chrome.runtime.onMessage.addListener(async (msg) => {
+ if (msg && msg.type) {
+ if (msg.currentTab && msg.type === MessageType.BOOKMARK_REFRESH_BADGE) {
+ console.log(
+ "Received REFRESH_BADGE message for tab:",
+ msg.currentTab.url,
+ );
+ if (msg.currentTab.url) {
+ await clearBadgeStatus(msg.currentTab.url);
+ }
+ if (typeof msg.currentTab.id === "number") {
+ await checkAndUpdateIcon(msg.currentTab.id);
+ }
+ }
+ }
+});