Skip to content

Commit

Permalink
Add functionality to Export notes
Browse files Browse the repository at this point in the history
A) See Options page to Export all notes (a single ZIP file).
B) Right-click on a note in the Sidebar to export that specific note only (HTML file).
  • Loading branch information
penge committed Oct 27, 2021
1 parent 593f2d5 commit 284fdc1
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 40 deletions.
7 changes: 6 additions & 1 deletion src/notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { sendMessage } from "messages";
import notesHistory from "notes/history";
import keyboardShortcuts, { KeyboardShortcut } from "notes/keyboard-shortcuts";
import { Command, commands } from "notes/commands";
import { exportNote } from "notes/export";

const getFocusOverride = (): boolean => new URL(window.location.href).searchParams.get("focus") === "";
const getActiveFromUrl = (): string => new URL(window.location.href).searchParams.get("note") || ""; // Bookmark
Expand Down Expand Up @@ -628,10 +629,14 @@ const Notes = (): h.JSX.Element => {
});
},
locked: notesProps.notes[noteName].locked ?? false,
toggleLocked: (noteName) => {
onToggleLocked: (noteName) => {
setContextMenuProps(null);
tabId && notesRef.current && setLocked(noteName, !(notesProps.notes[noteName].locked ?? false), tabId, notesRef.current);
},
onExport: (noteName) => {
setContextMenuProps(null);
exportNote(noteName);
},
})}
onNewNote={() => onNewNote()}
sync={sync}
Expand Down
8 changes: 5 additions & 3 deletions src/notes/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ export interface ContextMenuProps {
onRename: (noteName: string) => void
onDelete: (noteName: string) => void
locked: boolean
toggleLocked: (noteName: string) => void
onToggleLocked: (noteName: string) => void
onExport: (noteName: string) => void
}

const ContextMenu = ({
noteName, x, y, onRename, onDelete, locked, toggleLocked,
noteName, x, y, onRename, onDelete, locked, onToggleLocked, onExport,
}: ContextMenuProps): h.JSX.Element => {
const [offsetHeight, setOffsetHeight] = useState<number>(0);
const ref = useRef<HTMLDivElement>(null);
Expand All @@ -40,7 +41,8 @@ const ContextMenu = ({
}}>
<div class={clsx("action", locked && "disabled")} onClick={() => !locked && onRename(noteName)}>Rename</div>
<div class={clsx("action", locked && "disabled")} onClick={() => !locked && onDelete(noteName)}>Delete</div>
<div class="action" onClick={() => toggleLocked(noteName)}>{locked ? "Unlock" : "Lock"}</div>
<div class="action" onClick={() => onToggleLocked(noteName)}>{locked ? "Unlock" : "Lock"}</div>
<div class="action" onClick={() => onExport(noteName)}>Export</div>
</div>
);
};
Expand Down
31 changes: 31 additions & 0 deletions src/notes/export/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { zipSync, Zippable } from "fflate";
import { NotesObject } from "shared/storage/schema";
import { downloadBlob } from "shared/download";

export const exportNote = (noteName: string): void => chrome.storage.local.get("notes", (local) => {
const { notes } = local as { notes: NotesObject };
if (!(noteName in notes)) {
return; // there is no note to export
}

const encoder = new TextEncoder();
const data = encoder.encode(notes[noteName].content);

downloadBlob(data, `${noteName}.html`, "text/html");
});

export const exportNotes = (): void => chrome.storage.local.get("notes", (local) => {
const { notes } = local as { notes: NotesObject };
if (!Object.keys(notes).length) {
return; // there are no notes to export
}

const encoder = new TextEncoder();
const data: Zippable = Object.keys(notes).reduce((acc, curr) => {
acc[`${curr}.html`] = encoder.encode(notes[curr].content);
return acc;
}, {} as Zippable);

const gzipped = zipSync(data, { level: 0 });
downloadBlob(gzipped, "notes.zip", "application/zip");
});
12 changes: 12 additions & 0 deletions src/options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import __Size from "options/Size";
import __Theme from "options/Theme";
import __KeyboardShortcuts from "options/KeyboardShortcuts";
import __Options from "options/Options";
import __Export from "options/Export";
import __Version from "options/Version";

import {
Os,
Storage,
NotesObject,
RegularFont,
GoogleFont,
Theme,
Expand All @@ -21,6 +23,7 @@ import { setTheme as setThemeCore } from "themes/set-theme";
const Options = (): h.JSX.Element => {
const [os, setOs] = useState<Os | undefined>(undefined);
const [version] = useState<string>(chrome.runtime.getManifest().version);
const [notesCount, setNotesCount] = useState<number>(0);
const [font, setFont] = useState<RegularFont | GoogleFont | undefined>(undefined);
const [size, setSize] = useState<number>(0);
const [theme, setTheme] = useState<Theme | undefined>(undefined);
Expand All @@ -34,6 +37,7 @@ const Options = (): h.JSX.Element => {
chrome.runtime.getPlatformInfo((platformInfo) => setOs(platformInfo.os === "mac" ? "mac" : "other"));

chrome.storage.local.get([
"notes",
"font",
"size",
"theme",
Expand All @@ -45,6 +49,7 @@ const Options = (): h.JSX.Element => {
], items => {
const local = items as Storage;

setNotesCount(Object.keys(local.notes).length);
setFont(local.font);
setSize(local.size);
setTheme(local.theme);
Expand All @@ -60,6 +65,12 @@ const Options = (): h.JSX.Element => {
return;
}

if (changes["notes"]) {
const newValue: NotesObject = changes["notes"].newValue;
const newNotesCount = Object.keys(newValue).length;
setNotesCount(newNotesCount);
}

if (changes["font"]) {
setFont(changes["font"].newValue);
}
Expand Down Expand Up @@ -116,6 +127,7 @@ const Options = (): h.JSX.Element => {
tab={tab}
tabSize={tabSize}
/>
<__Export canExport={notesCount > 0} />
<__Version version={version} />
</Fragment>
);
Expand Down
31 changes: 31 additions & 0 deletions src/options/Export.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { h, Fragment } from "preact";
import clsx from "clsx";
import { exportNotes } from "notes/export";

let locked = false;

interface ExportProps {
canExport: boolean
}

const Export = ({ canExport }: ExportProps): h.JSX.Element => (
<Fragment>
<h2>Export</h2>
<input
type="button"
class={clsx("bold", "button", !canExport && "disabled")}
value="Export all notes"
onClick={() => {
if (!canExport || locked) {
return;
}

locked = true;
exportNotes();
window.setTimeout(() => locked = false, 1000);
}}
/>
</Fragment>
);

export default Export;
4 changes: 2 additions & 2 deletions src/options/Font.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const Font = ({ font }: FontProps): h.JSX.Element => {
<input
type="text"
placeholder="Font Name (E.g. Roboto Mono)"
class="input"
value={googleFontName}
onInput={(event) => {
setGoogleFontName((event.target as HTMLInputElement).value);
Expand All @@ -96,8 +97,7 @@ const Font = ({ font }: FontProps): h.JSX.Element => {
/>
<input
type="submit"
id="submit"
class={clsx("bold", (googleFontName !== font?.name) && "active")}
class={clsx("bold", "button", (googleFontName === font?.name) && "disabled")}
value={googleSubmitButtonText}
onClick={() => {
const trimmedGoogleFontName = googleFontName.trim();
Expand Down
2 changes: 1 addition & 1 deletion src/options/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const Options = ({ sync, autoSync, tab, tabSize }: OptionsProps): h.JSX.Element
</div>
<div class={clsx("space-top", !tab && "disabled")}>
<label>Tab size:</label>
<select name="tab-size" value={tabSize} onChange={(event) => {
<select name="tab-size" class="select" value={tabSize} onChange={(event) => {
const newTabSize: number = parseInt((event.target as HTMLSelectElement).value);
chrome.storage.local.set({ tabSize: newTabSize });
}}>
Expand Down
17 changes: 17 additions & 0 deletions src/shared/download/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const downloadURL = (data: string, fileName: string): void => {
const a = document.createElement("a");
a.href = data;
a.download = fileName;
a.click();
a.remove();
};

export const downloadBlob = (data: Uint8Array, fileName: string, mimeType: string): void => {
const blob = new Blob([data], {
type: mimeType,
});

const url = window.URL.createObjectURL(blob);
downloadURL(url, fileName);
window.setTimeout(() => window.URL.revokeObjectURL(url), 2000);
};
66 changes: 33 additions & 33 deletions static/options.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,38 @@ input[type="radio"], input[type='checkbox'] {
margin: 0 10px 0 2px;
}

select { border-radius: 3px; }

.separator { padding: 0 12px; }
.bold { font-weight: bold; }
.space-top { margin-top: 1em; }
.space-left { margin-left: 1em; }

.disabled {
opacity: .3;
user-select: none;
pointer-events: none;
}

/* Inputs */

.button {
cursor: pointer;
}

.button, .input, .select {
max-width: 500px;
box-sizing: content-box;
outline: none;
border: 1px solid silver;
padding: 12px;
border-radius: 3px;
}

#dark .button,
#dark .input,
#dark .select {
border-color: transparent;
}

/* Selection */

.selection { padding: 10px 0; }
Expand Down Expand Up @@ -65,9 +90,6 @@ body#dark .comment {
.font-category { cursor: pointer; }
.font-category.active { text-decoration: underline; }

#submit { opacity: .3; }
#submit.active { opacity: 1; cursor: pointer; }

.font-area .selection {
font-size: 80%;
display: flex;
Expand All @@ -82,26 +104,14 @@ body#dark .comment {
#google-fonts-area ol { margin-top: 0; }
#google-fonts-area ol li { line-height: 2em; }

#google-fonts-area input, select {
max-width: 500px;
box-sizing: content-box;
outline: none;
border: 1px solid silver;
padding: 12px;
}

#dark #google-fonts-area input, select {
border-color: transparent;
#google-fonts-area .input {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}

#google-fonts-area input[type="text"] {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}

#google-fonts-area input[type="submit"] {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
#google-fonts-area .button {
border-top-left-radius: 0;
border-top-right-radius: 0;
}

/* Font size */
Expand Down Expand Up @@ -189,16 +199,6 @@ body#dark .comment {
word-break: break-all;
}

div.disabled {
opacity: .3;
user-select: none;
}

div.disabled input,
div.disabled select {
pointer-events: none;
}

/* Media Queries */

@media only screen and (max-width: 600px) {
Expand Down

0 comments on commit 284fdc1

Please sign in to comment.