Skip to content

Commit

Permalink
Merge pull request #395 from penge/save-image-to-note
Browse files Browse the repository at this point in the history
Save image to note using context menu
  • Loading branch information
penge authored Jul 28, 2022
2 parents 944926b + 19c1d47 commit 2b63284
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 122 deletions.
8 changes: 7 additions & 1 deletion src/background.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import setId from "background/init/set-id";
import runMigrations from "background/init/migrations/run-migrations";
import { showNewVersionNotification } from "background/init/notifications";
import {
showNewVersionNotification,
attachNotificationsOnClicked,
} from "background/init/notifications";

import {
openMyNotesOnIconClick,
Expand Down Expand Up @@ -41,6 +44,9 @@ saveTextOnRemoteTransfer(); // when you use "Save to remotely open My Notes" fro
// Permissions
handleChangedPermissions(); // react to granted/removed optional permissions: "identity"

// Notifications
attachNotificationsOnClicked();

// Run when Installed or Updated
chrome.runtime.onInstalled.addListener((details) => {
setId(); // Set unique My Notes ID, if not set before
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import getTextToSave from "../get-text-to-save";

const irrelevant: Pick<chrome.contextMenus.OnClickData, "menuItemId" | "editable"> = {
menuItemId: "",
editable: true,
};

describe("getTextToSave()", () => {
describe("page context", () => {
it("returns url to save", () => {
expect(getTextToSave("page", {
...irrelevant,
pageUrl: "https://github.com/penge/my-notes",
})).toBe('<a href="https://github.com/penge/my-notes" target="_blank">https://github.com/penge/my-notes</a>'
+ "<br><br>");
});
});

describe("image context", () => {
it("returns img to save", () => {
expect(getTextToSave("image", {
...irrelevant,
pageUrl: "https://domain.com",
srcUrl: "https://domain.com/image.png",
})).toBe('<img src="https://domain.com/image.png">'
+ "<br><br>");
});
});

describe("selection context", () => {
it("returns selection to save", () => {
expect(getTextToSave("selection", {
...irrelevant,
pageUrl: "https://articles.com/good-article",
selectionText: "Text selected from the article...",
})).toBe("Text selected from the article...<br>"
+ '<b>(<a href="https://articles.com/good-article" target="_blank">https://articles.com/good-article</a>)</b>'
+ "<br><br>");
});
});
});
20 changes: 20 additions & 0 deletions src/background/init/context-menu/__tests__/is-note-locked.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NotesObject, Note } from "shared/storage/schema";
import isNoteLocked from "../is-note-locked";

const dummyNote: Note = {
content: "",
createdTime: "",
modifiedTime: "",
};

const notes: NotesObject = {
"@clipboard": { ...dummyNote, locked: false },
"@images": { ...dummyNote, locked: true },
Todo: { ...dummyNote, locked: undefined },
};

test("isNoteLocked()", () => {
expect(isNoteLocked(notes, "@clipboard")).toBe(false);
expect(isNoteLocked(notes, "@images")).toBe(true);
expect(isNoteLocked(notes, "Todo")).toBe(false);
});
41 changes: 41 additions & 0 deletions src/background/init/context-menu/__tests__/split-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import splitId, { SplitIdResult } from "../split-id";

type T = SplitIdResult<chrome.contextMenus.ContextType, "note" | "remote">;

test("splitId()", () => {
expect(splitId("page-note-@clipboard")).toEqual<T>({
context: "page",
destination: "note",
noteName: "@clipboard",
});

expect(splitId("page-note-Todo")).toEqual<T>({
context: "page",
destination: "note",
noteName: "Todo",
});

expect(splitId("page-note-Tables and Chairs")).toEqual<T>({
context: "page",
destination: "note",
noteName: "Tables and Chairs",
});

expect(splitId("page-note-npm-packages")).toEqual<T>({
context: "page",
destination: "note",
noteName: "npm-packages",
});

expect(splitId("image-note-@images")).toEqual<T>({
context: "image",
destination: "note",
noteName: "@images",
});

expect(splitId("selection-remote")).toEqual<T>({
context: "selection",
destination: "remote",
noteName: "",
});
});
7 changes: 7 additions & 0 deletions src/background/init/context-menu/get-text-to-save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ const getUrlToSave: Handler = (info) => {
return toSave;
};

const getImageToSave: Handler = (info) => {
const { srcUrl } = info;
const toSave = `<img src="${srcUrl}"><br><br>`;
return toSave;
};

const getSelectionToSave: Handler = (info) => {
const { pageUrl, selectionText } = info;
const pageUrlHtml = getPageUrlHtml(pageUrl);
Expand All @@ -18,6 +24,7 @@ const getSelectionToSave: Handler = (info) => {

const handlers: Partial<Record<chrome.contextMenus.ContextType, Handler>> = {
page: getUrlToSave,
image: getImageToSave,
selection: getSelectionToSave,
};

Expand Down
156 changes: 49 additions & 107 deletions src/background/init/context-menu/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import { NotesObject } from "shared/storage/schema";
import { tString } from "i18n";
import isNoteLocked from "./is-note-locked";
import splitId from "./split-id";
import getTextToSave from "./get-text-to-save";
import {
CLIPBOARD_NOTE_NAME,
saveTextToLocalMyNotes,
saveTextToRemotelyOpenMyNotes,
} from "../saving";
import { notify } from "../notifications";
import { CLIPBOARD_NOTE_NAME, IMAGES_NOTE_NAME } from "../reserved-note-names";

const ID = "my-notes";

const PAGE_NOTE_PREFIX = "page-note-";
const PAGE_REMOTE_MY_NOTES = "page-remote-my-notes";

const SELECTION_NOTE_PREFIX = "selection-note-";
const SELECTION_REMOTE_MY_NOTES = "selection-remote-my-notes";

const isLocked = (notes: NotesObject, noteName: string): boolean => !!(notes[noteName]?.locked);
const contexts: chrome.contextMenus.ContextType[] = ["page", "image", "selection"];

/**
* Creates My Notes Context menu
Expand All @@ -29,135 +24,82 @@ const isLocked = (notes: NotesObject, noteName: string): boolean => !!(notes[not
const createContextMenu = (notes: NotesObject): string | number => chrome.contextMenus.create({
id: ID,
title: "My Notes",
contexts: ["page", "selection"],
}, () => {
const forEveryNoteExceptClipboard = (callback: (noteName: string) => void) => {
Object.keys(notes).filter((noteName) => noteName !== CLIPBOARD_NOTE_NAME).sort().forEach(callback);
};

/* -------------< page >------------- */

const pageCommonProperties: chrome.contextMenus.CreateProperties = {
contexts,
}, () => contexts.forEach((context) => {
const reservedNoteNames = [
CLIPBOARD_NOTE_NAME,
context === "image" ? IMAGES_NOTE_NAME : "",
].filter(Boolean);

const nonReservedNoteNames = Object.keys(notes)
.filter((noteName) => !reservedNoteNames.includes(noteName))
.sort();

const commonProperties: chrome.contextMenus.CreateProperties = {
parentId: ID,
contexts: ["page"],
contexts: [context],
};

chrome.contextMenus.create({
...pageCommonProperties,
id: [PAGE_NOTE_PREFIX, CLIPBOARD_NOTE_NAME].join(""),
title: tString("Context Menu.menus.Save URL to", { note: CLIPBOARD_NOTE_NAME }),
enabled: !isLocked(notes, CLIPBOARD_NOTE_NAME),
const noteProperties = (noteName: string): chrome.contextMenus.CreateProperties => ({
...commonProperties,
id: `${context}-note-${noteName}`,
title: tString(`Context Menu.${context}-note.title`, { note: noteName }),
enabled: !isNoteLocked(notes, noteName),
});

chrome.contextMenus.create({
...pageCommonProperties,
const separatorProperties = (suffix: string): chrome.contextMenus.CreateProperties => ({
...commonProperties,
type: "separator",
id: "page-separator-one",
id: `${context}-separator-${suffix}`,
});

forEveryNoteExceptClipboard((noteName) => {
chrome.contextMenus.create({
...pageCommonProperties,
id: [PAGE_NOTE_PREFIX, noteName].join(""),
title: tString("Context Menu.menus.Save URL to", { note: noteName }),
enabled: !isLocked(notes, noteName),
});
reservedNoteNames.forEach((noteName) => {
chrome.contextMenus.create(noteProperties(noteName));
});

chrome.contextMenus.create({
...pageCommonProperties,
type: "separator",
id: "page-separator-two",
});
chrome.contextMenus.create(separatorProperties("first"));

chrome.contextMenus.create({
...pageCommonProperties,
id: PAGE_REMOTE_MY_NOTES,
title: tString("Context Menu.menus.Save URL to remotely open My Notes"),
nonReservedNoteNames.forEach((noteName) => {
chrome.contextMenus.create(noteProperties(noteName));
});

/* -------------< selection >------------- */

const selectionCommonProperties: chrome.contextMenus.CreateProperties = {
parentId: ID,
contexts: ["selection"],
};

chrome.contextMenus.create({
...selectionCommonProperties,
id: [SELECTION_NOTE_PREFIX, CLIPBOARD_NOTE_NAME].join(""),
title: tString("Context Menu.menus.Save to", { note: CLIPBOARD_NOTE_NAME }),
enabled: !isLocked(notes, CLIPBOARD_NOTE_NAME),
});
chrome.contextMenus.create(separatorProperties("second"));

chrome.contextMenus.create({
...selectionCommonProperties,
type: "separator",
id: "selection-separator-one",
});

forEveryNoteExceptClipboard((noteName) => {
chrome.contextMenus.create({
...selectionCommonProperties,
id: [SELECTION_NOTE_PREFIX, noteName].join(""),
title: tString("Context Menu.menus.Save to", { note: noteName }),
enabled: !isLocked(notes, noteName),
});
...commonProperties,
id: `${context}-remote`,
title: tString(`Context Menu.${context}-remote.title`),
});

chrome.contextMenus.create({
...selectionCommonProperties,
type: "separator",
id: "selection-separator-two",
});

chrome.contextMenus.create({
...selectionCommonProperties,
id: SELECTION_REMOTE_MY_NOTES,
title: tString("Context Menu.menus.Save to remotely open My Notes"),
});
});

let currentNotesString: string;
}));

export const attachContextMenuOnClicked = (): void => chrome.contextMenus.onClicked.addListener((info) => {
const menuId: string = info.menuItemId.toString();
const context = menuId.split("-")[0] as chrome.contextMenus.ContextType;
const { context, destination, noteName } = splitId<chrome.contextMenus.ContextType, "note" | "remote">(info.menuItemId.toString());
const tKey = `Context Menu.${context}-${destination}.notification`;
const textToSave = getTextToSave(context, info);
if (!textToSave) {
return;
}

/* -------------< page >------------- */

if (menuId.startsWith(PAGE_NOTE_PREFIX)) {
const noteName = menuId.replace(PAGE_NOTE_PREFIX, "");
if (destination === "note" && noteName) {
saveTextToLocalMyNotes(textToSave, noteName);
notify(tString("Context Menu.notifications.Saved URL to", { note: noteName }));
return;
}

if (menuId === PAGE_REMOTE_MY_NOTES) {
saveTextToRemotelyOpenMyNotes(textToSave);
notify(tString("Context Menu.notifications.Sent URL to remotely open My Notes"));
return;
}

/* -------------< selection >------------- */

if (menuId.startsWith(SELECTION_NOTE_PREFIX)) {
const noteName = menuId.replace(SELECTION_NOTE_PREFIX, "");
saveTextToLocalMyNotes(textToSave, noteName);
notify(tString("Context Menu.notifications.Saved text to", { note: noteName }));
notify({
notificationId: `note-${new Date().getTime()}-${noteName}`,
message: tString(tKey, { note: noteName }),
});
return;
}

if (menuId === SELECTION_REMOTE_MY_NOTES) {
if (destination === "remote") {
saveTextToRemotelyOpenMyNotes(textToSave);
notify(tString("Context Menu.notifications.Sent text to remotely open My Notes"));
notify({
notificationId: `remote-${new Date().getTime()}`,
message: tString(tKey),
});
}
});

let currentNotesString: string;

const recreateContextMenuFromNotes = (notes: NotesObject | undefined): void => {
if (!notes) {
currentNotesString = "";
Expand Down
3 changes: 3 additions & 0 deletions src/background/init/context-menu/is-note-locked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NotesObject } from "shared/storage/schema";

export default (notes: NotesObject, noteName: string): boolean => !!(notes[noteName]?.locked);
14 changes: 14 additions & 0 deletions src/background/init/context-menu/split-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type SplitIdResult<C extends unknown, D extends unknown> = {
context: C
destination: D
noteName: string
};

export default <C extends unknown, D extends unknown>(id: string): SplitIdResult<C, D> => {
const [context, destination, ...noteNameParts] = id.split("-") as [C, D, string[]];
return {
context,
destination,
noteName: noteNameParts.join("-"),
};
};
Loading

0 comments on commit 2b63284

Please sign in to comment.