mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
mobile: export notes
This commit is contained in:
@@ -39,6 +39,7 @@ import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { SIZE, ph, pv } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { ProTag } from "../../premium/pro-tag";
|
||||
import { Button } from "../../ui/button";
|
||||
@@ -47,7 +48,6 @@ import { Pressable } from "../../ui/pressable";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { Dialog } from "../../dialog";
|
||||
|
||||
const ExportNotesSheet = ({
|
||||
ids,
|
||||
@@ -71,7 +71,7 @@ const ExportNotesSheet = ({
|
||||
const [status, setStatus] = useState<string>();
|
||||
const premium = useUserStore((state) => state.premium);
|
||||
|
||||
const save = async (
|
||||
const exportNoteAs = async (
|
||||
type: "pdf" | "txt" | "md" | "html" | "md-frontmatter"
|
||||
) => {
|
||||
if (exporting) return;
|
||||
@@ -81,9 +81,18 @@ const ExportNotesSheet = ({
|
||||
setComplete(false);
|
||||
let result;
|
||||
if (ids.length > 1) {
|
||||
result = await Exporter.bulkExport(ids, type, setStatus);
|
||||
result = await Exporter.bulkExport(
|
||||
db.notes.all.where((eb) => eb("id", "in", ids)),
|
||||
type,
|
||||
setStatus
|
||||
);
|
||||
} else {
|
||||
result = await Exporter.exportNote(ids[0], type);
|
||||
const note = await db.notes.note(ids[0]);
|
||||
if (!note) {
|
||||
setExporting(false);
|
||||
return;
|
||||
}
|
||||
result = await Exporter.exportNote(note, type, setStatus);
|
||||
await sleep(1000);
|
||||
}
|
||||
if (!result) {
|
||||
@@ -101,7 +110,7 @@ const ExportNotesSheet = ({
|
||||
{
|
||||
title: "PDF",
|
||||
func: async () => {
|
||||
await save("pdf");
|
||||
await exportNoteAs("pdf");
|
||||
},
|
||||
icon: "file-pdf-box",
|
||||
desc: "View in any pdf reader app",
|
||||
@@ -111,7 +120,7 @@ const ExportNotesSheet = ({
|
||||
{
|
||||
title: "Markdown",
|
||||
func: async () => {
|
||||
await save("md");
|
||||
await exportNoteAs("md");
|
||||
},
|
||||
icon: "language-markdown",
|
||||
desc: "View in any text or markdown editor",
|
||||
@@ -121,7 +130,7 @@ const ExportNotesSheet = ({
|
||||
{
|
||||
title: "Markdown + Frontmatter",
|
||||
func: async () => {
|
||||
await save("md-frontmatter");
|
||||
await exportNoteAs("md-frontmatter");
|
||||
},
|
||||
icon: "language-markdown",
|
||||
desc: "View in any text or markdown editor",
|
||||
@@ -131,7 +140,7 @@ const ExportNotesSheet = ({
|
||||
{
|
||||
title: "Plain Text",
|
||||
func: async () => {
|
||||
await save("txt");
|
||||
await exportNoteAs("txt");
|
||||
},
|
||||
icon: "card-text",
|
||||
desc: "View in any text editor",
|
||||
@@ -141,7 +150,7 @@ const ExportNotesSheet = ({
|
||||
{
|
||||
title: "HTML",
|
||||
func: async () => {
|
||||
await save("html");
|
||||
await exportNoteAs("html");
|
||||
},
|
||||
icon: "language-html5",
|
||||
desc: "View in any web browser & html reader",
|
||||
@@ -250,11 +259,9 @@ const ExportNotesSheet = ({
|
||||
<>
|
||||
<ActivityIndicator />
|
||||
<Paragraph>
|
||||
{ids.length === 1
|
||||
? "Exporting note... Please wait"
|
||||
: `Exporting notes${
|
||||
status ? ` (${status})` : ``
|
||||
}... Please wait`}
|
||||
{`${
|
||||
status ? `${status})` : `Exporting notes`
|
||||
}... Please wait`}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -26,13 +26,23 @@ import { zip } from "react-native-zip-archive";
|
||||
import { DatabaseLogger, db } from "../common/database/index";
|
||||
import Storage from "../common/database/storage";
|
||||
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import {
|
||||
exportNote as _exportNote,
|
||||
ExportableAttachment,
|
||||
ExportableNote,
|
||||
exportNotes,
|
||||
sanitizeFilename
|
||||
} from "@notesnook/common";
|
||||
import { Note } from "@notesnook/core";
|
||||
import { NoteContent } from "@notesnook/core/dist/collections/session-content";
|
||||
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
|
||||
import { basename, dirname, join } from "pathe";
|
||||
import downloadAttachment from "../common/filesystem/download-attachment";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import BiometicService from "./biometrics";
|
||||
import { ToastManager } from "./event-manager";
|
||||
import { cacheDir } from "../common/filesystem/utils";
|
||||
|
||||
const MIMETypes = {
|
||||
txt: "text/plain",
|
||||
@@ -222,49 +232,6 @@ async function unlockVault() {
|
||||
});
|
||||
}
|
||||
|
||||
async function exportNote(
|
||||
id: string,
|
||||
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter"
|
||||
) {
|
||||
const note = await db.notes.note(id);
|
||||
if (!note) return;
|
||||
|
||||
let content;
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if (locked) {
|
||||
try {
|
||||
const unlocked = await unlockVault();
|
||||
if (!unlocked) return null;
|
||||
const unlockedNote = await db.vault.open(note.id);
|
||||
content = unlockedNote?.content;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
let path = await getPath(FolderNames[type]);
|
||||
if (!path) return;
|
||||
|
||||
const result = await exportAs(type, note, false, content);
|
||||
if (!result) return null;
|
||||
const fileName = sanitizeFilename(note.title + Date.now(), {
|
||||
replacement: "_"
|
||||
});
|
||||
|
||||
if (type === "pdf" && Platform.OS === "ios") {
|
||||
path = result;
|
||||
} else {
|
||||
path = await save(path, result, fileName, type);
|
||||
}
|
||||
|
||||
return {
|
||||
filePath: path,
|
||||
type: "text/plain",
|
||||
name: "Text",
|
||||
fileName: fileName + `.${type === "md-frontmatter" ? "md" : type}`
|
||||
};
|
||||
}
|
||||
|
||||
function copyFileAsync(source: string, dest: string) {
|
||||
return new Promise((resolve) => {
|
||||
ScopedStorage.copyFile(source, dest, () => {
|
||||
@@ -273,128 +240,59 @@ function copyFileAsync(source: string, dest: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function getUniqueFileName(
|
||||
fileName: string,
|
||||
notebookPath: string,
|
||||
results: { [name: string]: boolean }
|
||||
async function resolveFileFunctions(
|
||||
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter"
|
||||
) {
|
||||
const chunks = fileName.split(".");
|
||||
const ext = chunks.pop();
|
||||
const name = chunks.join(".");
|
||||
let resolvedName = fileName;
|
||||
let count = 0;
|
||||
while (results[`${notebookPath}${resolvedName}`]) {
|
||||
resolvedName = `${name}${++count}.${ext}`;
|
||||
}
|
||||
|
||||
return resolvedName;
|
||||
}
|
||||
|
||||
async function bulkExport(
|
||||
ids: string[],
|
||||
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter",
|
||||
callback: (progress?: string) => void
|
||||
) {
|
||||
let path = await getPath(FolderNames[type]);
|
||||
const path = await getPath(FolderNames[type]);
|
||||
if (!path) return;
|
||||
|
||||
const exportCacheFolder =
|
||||
RNFetchBlob.fs.dirs.CacheDir + `/export_${Date.now()}`;
|
||||
const exportCacheFolder = join(
|
||||
RNFetchBlob.fs.dirs.CacheDir,
|
||||
`/export_${Date.now()}`
|
||||
);
|
||||
|
||||
await RNFetchBlob.fs.mkdir(exportCacheFolder).catch((e) => console.log(e));
|
||||
|
||||
const mkdir = async (dir: string) => {
|
||||
const folder = `${exportCacheFolder}/${dir}`;
|
||||
const folder = join(exportCacheFolder, dir);
|
||||
if (!(await RNFetchBlob.fs.exists(folder))) {
|
||||
await RNFetchBlob.fs.mkdir(folder);
|
||||
}
|
||||
};
|
||||
|
||||
const writeFile = async (path: string, result: string) => {
|
||||
const cacheFilePath = exportCacheFolder + path;
|
||||
const cacheFilePath = join(exportCacheFolder, path);
|
||||
console.log(cacheFilePath, result.length);
|
||||
await RNFetchBlob.fs.writeFile(
|
||||
cacheFilePath,
|
||||
result,
|
||||
type === "pdf" ? "base64" : "utf8"
|
||||
);
|
||||
};
|
||||
return {
|
||||
path,
|
||||
cacheFolder: exportCacheFolder,
|
||||
mkdir,
|
||||
writeFile
|
||||
};
|
||||
}
|
||||
|
||||
const results: { [name: string]: boolean } = {};
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
try {
|
||||
const note = await db.notes.note(ids[i]);
|
||||
if (!note) continue;
|
||||
|
||||
let content;
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if (locked) {
|
||||
try {
|
||||
const unlocked = !db.vault.unlocked ? await unlockVault() : true;
|
||||
if (!unlocked) {
|
||||
continue;
|
||||
}
|
||||
const unlockedNote = await db.vault.open(note.id);
|
||||
content = unlockedNote?.content;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await exportAs(type, note, true, content);
|
||||
const fileName = sanitizeFilename(note.title, {
|
||||
replacement: "_"
|
||||
});
|
||||
if (result) {
|
||||
const notebooks = await db.relations
|
||||
?.to({ id: note.id, type: "note" }, "notebook")
|
||||
.resolve();
|
||||
|
||||
for (const notebook of notebooks) {
|
||||
const notebookPath = (await db.notebooks.breadcrumbs(notebook.id))
|
||||
.map((notebook) => {
|
||||
return notebook.title + "/";
|
||||
})
|
||||
.join("");
|
||||
|
||||
await mkdir(notebookPath);
|
||||
|
||||
console.log("Dir created", notebookPath);
|
||||
|
||||
const exportedNoteName = getUniqueFileName(
|
||||
fileName + `.${type}`,
|
||||
notebookPath,
|
||||
results
|
||||
);
|
||||
results[`${notebookPath}${exportedNoteName}`] = true;
|
||||
await writeFile(`/${notebookPath}${exportedNoteName}`, result);
|
||||
}
|
||||
|
||||
if (!notebooks.length) {
|
||||
const exportedNoteName = getUniqueFileName(
|
||||
fileName + `.${type}`,
|
||||
"",
|
||||
results
|
||||
);
|
||||
results[exportedNoteName] = true;
|
||||
await writeFile(`/${exportedNoteName}`, result);
|
||||
}
|
||||
}
|
||||
callback(`${i + 1}/${ids.length}`);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
}
|
||||
}
|
||||
const fileName = `nn-export-${ids.length}-${type}-${Date.now()}.zip`;
|
||||
|
||||
async function createZip(
|
||||
totalNotes: number,
|
||||
cacheFolder: string,
|
||||
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter",
|
||||
path: string,
|
||||
callback: (progress?: string) => void
|
||||
) {
|
||||
const fileName = `nn-export-${totalNotes}-${type}-${Date.now()}.zip`;
|
||||
try {
|
||||
callback("zipping");
|
||||
callback("Creating zip");
|
||||
const zipOutputPath =
|
||||
Platform.OS === "ios"
|
||||
? path + `/${fileName}`
|
||||
: RNFetchBlob.fs.dirs.CacheDir + `/${fileName}`;
|
||||
await zip(exportCacheFolder, zipOutputPath);
|
||||
callback("saving to disk");
|
||||
? join(path, fileName)
|
||||
: join(RNFetchBlob.fs.dirs.CacheDir, fileName);
|
||||
await zip(cacheFolder, zipOutputPath);
|
||||
callback("Saving zip file");
|
||||
if (Platform.OS === "android") {
|
||||
const file = await ScopedStorage.createFile(
|
||||
path,
|
||||
@@ -406,7 +304,7 @@ async function bulkExport(
|
||||
await RNFetchBlob.fs.unlink(zipOutputPath);
|
||||
callback();
|
||||
}
|
||||
RNFetchBlob.fs.unlink(exportCacheFolder);
|
||||
RNFetchBlob.fs.unlink(cacheFolder);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
}
|
||||
@@ -416,10 +314,152 @@ async function bulkExport(
|
||||
type: "application/zip",
|
||||
name: "zip",
|
||||
fileName: fileName,
|
||||
count: results?.length || 0
|
||||
count: totalNotes
|
||||
};
|
||||
}
|
||||
|
||||
async function exportNoteToFile(
|
||||
item: ExportableNote,
|
||||
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter",
|
||||
mkdir: (dir: string) => Promise<void>,
|
||||
writeFile: (path: string, result: string) => Promise<void>
|
||||
) {
|
||||
const dir = dirname(item.path);
|
||||
const filename = basename(item.path);
|
||||
await mkdir(dir);
|
||||
if (type === "pdf") {
|
||||
const options = {
|
||||
html: item.data,
|
||||
fileName: Platform.OS === "ios" ? "/exported/PDF/" + filename : filename,
|
||||
width: 595,
|
||||
height: 852,
|
||||
bgColor: "#FFFFFF",
|
||||
padding: 30,
|
||||
base64: true
|
||||
} as { [name: string]: any };
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
options.directory = "Documents";
|
||||
}
|
||||
const res = await RNHTMLtoPDF.convert(options);
|
||||
if (res.filePath) {
|
||||
RNFetchBlob.fs.unlink(res.filePath);
|
||||
}
|
||||
await writeFile(item.path, res.base64);
|
||||
} else {
|
||||
await writeFile(item.path, item.data);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportAttachmentToFile(
|
||||
item: ExportableAttachment,
|
||||
mkdir: (dir: string) => Promise<void>,
|
||||
cacheFolder: string
|
||||
) {
|
||||
const dir = dirname(item.path);
|
||||
await mkdir(dir);
|
||||
const cacheFileName = await downloadAttachment(item.data.hash, true, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
throwError: false
|
||||
} as any);
|
||||
|
||||
await RNFetchBlob.fs.mv(
|
||||
join(cacheDir, cacheFileName as string),
|
||||
join(cacheFolder, item.path)
|
||||
);
|
||||
}
|
||||
|
||||
async function bulkExport(
|
||||
notes: FilteredSelector<Note>,
|
||||
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter",
|
||||
callback: (progress?: string) => void
|
||||
) {
|
||||
const totalNotes = await notes.count();
|
||||
|
||||
const fileFuncions = await resolveFileFunctions(type);
|
||||
|
||||
if (!fileFuncions) return;
|
||||
|
||||
const path = fileFuncions.path;
|
||||
const { mkdir, writeFile, cacheFolder } = fileFuncions;
|
||||
|
||||
let currentProgress = 0;
|
||||
let currentAttachmentProgress = 0;
|
||||
for await (const item of exportNotes(notes, {
|
||||
format: type,
|
||||
unlockVault: unlockVault as () => Promise<boolean>
|
||||
})) {
|
||||
if (item instanceof Error) {
|
||||
DatabaseLogger.error(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.type === "note") {
|
||||
currentProgress += 1;
|
||||
callback(`Exporting notes (${currentProgress}/${totalNotes})`);
|
||||
try {
|
||||
await exportNoteToFile(item, type, mkdir, writeFile);
|
||||
} catch (e) {
|
||||
console.log(item.type, e);
|
||||
}
|
||||
} else if (item.type === "attachment") {
|
||||
currentAttachmentProgress += 1;
|
||||
callback(`Downloading attachments (${currentAttachmentProgress})`);
|
||||
try {
|
||||
await exportAttachmentToFile(item, mkdir, cacheFolder);
|
||||
} catch (e) {
|
||||
console.log(item.path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(cacheFolder);
|
||||
return createZip(totalNotes, cacheFolder, type, path, callback);
|
||||
}
|
||||
|
||||
async function exportNote(
|
||||
note: Note,
|
||||
type: "txt" | "pdf" | "md" | "html" | "md-frontmatter",
|
||||
callback: (progress?: string) => void
|
||||
) {
|
||||
const fileFuncions = await resolveFileFunctions(type);
|
||||
|
||||
if (!fileFuncions) return;
|
||||
|
||||
const path = fileFuncions.path;
|
||||
const { mkdir, writeFile, cacheFolder } = fileFuncions;
|
||||
|
||||
let currentAttachmentProgress = 0;
|
||||
for await (const item of _exportNote(note, {
|
||||
format: type,
|
||||
unlockVault: unlockVault as () => Promise<boolean>
|
||||
})) {
|
||||
if (item instanceof Error) {
|
||||
DatabaseLogger.error(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.type === "note") {
|
||||
callback(`Exporting note`);
|
||||
try {
|
||||
await exportNoteToFile(item, type, mkdir, writeFile);
|
||||
} catch (e) {
|
||||
console.log("exportNoteToFile", item.type, e);
|
||||
}
|
||||
} else if (item.type === "attachment") {
|
||||
currentAttachmentProgress += 1;
|
||||
callback(`Downloading attachments (${currentAttachmentProgress})`);
|
||||
try {
|
||||
await exportAttachmentToFile(item, mkdir, cacheFolder);
|
||||
} catch (e) {
|
||||
console.log("exportAttachmentToFile", item.path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createZip(1, cacheFolder, type, path, callback);
|
||||
}
|
||||
|
||||
const Exporter = {
|
||||
exportNote,
|
||||
bulkExport
|
||||
|
||||
@@ -164,7 +164,8 @@ export async function* exportNote(
|
||||
const content = await exportContent(note, {
|
||||
format,
|
||||
attachmentsRoot,
|
||||
pendingAttachments
|
||||
pendingAttachments,
|
||||
unlockVault: options.unlockVault
|
||||
});
|
||||
if (!content) return false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user