143 lines
4.9 KiB
TypeScript
143 lines
4.9 KiB
TypeScript
import JSZip from "jszip";
|
|
import { saveAs } from "file-saver";
|
|
import { Page } from "./store";
|
|
import { useSettingsStore } from "./settings-store";
|
|
import { sanitizeFilename } from "./page-utils";
|
|
import { htmlToMarkdown } from "./markdown-codec";
|
|
|
|
// Helper to escape YAML strings
|
|
function escapeYamlString(str: string): string {
|
|
return str.replace(/"/g, '\\"');
|
|
}
|
|
|
|
/**
|
|
* Converts a page to Markdown with Frontmatter
|
|
*/
|
|
export function pageToMarkdown(page: Page): string {
|
|
const rawMarkdown = htmlToMarkdown(page.content || "");
|
|
const tags = page.tags ? JSON.parse(typeof page.tags === 'string' ? page.tags : JSON.stringify(page.tags)) : [];
|
|
|
|
// Format Date to Local Time with Timezone
|
|
const dateObj = page.updatedAt ? new Date(page.updatedAt) : new Date();
|
|
|
|
// Get timezone from store
|
|
const { timezone } = useSettingsStore.getState();
|
|
|
|
// Use Intl to format date in the target timezone
|
|
// We want format: YYYY-MM-DDTHH:mm:ss
|
|
// Since Intl isn't extremely flexible with custom formats without parts, we can use "sv-SE" locale which is usually ISO-like (YYYY-MM-DD HH:mm:ss)
|
|
// Or just construct it using parts.
|
|
|
|
let localDate;
|
|
try {
|
|
const options: Intl.DateTimeFormatOptions = {
|
|
timeZone: timezone || "Asia/Shanghai",
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false
|
|
};
|
|
// sv-SE outputs YYYY-MM-DD HH:mm:ss
|
|
const parts = new Intl.DateTimeFormat('sv-SE', options).formatToParts(dateObj);
|
|
const getPart = (type: string) => parts.find(p => p.type === type)?.value || "";
|
|
localDate = `${getPart('year')}-${getPart('month')}-${getPart('day')}T${getPart('hour')}:${getPart('minute')}:${getPart('second')}`;
|
|
} catch {
|
|
// Fallback if timezone is invalid
|
|
localDate = dateObj.toISOString().slice(0, 19);
|
|
}
|
|
|
|
// Construct Frontmatter
|
|
const frontmatter = [
|
|
"---",
|
|
`title: "${escapeYamlString(page.title)}"`,
|
|
`id: "${page.id}"`,
|
|
`date: "${localDate}"`,
|
|
`tags: [${tags.map((t: string) => `"${escapeYamlString(t)}"`).join(", ")}]`,
|
|
`order: ${page.order || 0}`,
|
|
"---",
|
|
"",
|
|
""
|
|
].join("\n");
|
|
|
|
return frontmatter + rawMarkdown;
|
|
}
|
|
|
|
/**
|
|
* Exports a single page as a Markdown file
|
|
*/
|
|
export function exportPageAsMarkdown(page: Page) {
|
|
const markdown = pageToMarkdown(page);
|
|
const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
|
|
const filename = sanitizeFilename(page.title || "Untitled");
|
|
saveAs(blob, `${filename}.md`);
|
|
}
|
|
|
|
/**
|
|
* Exports a folder and its contents as a Zip file
|
|
*/
|
|
export async function exportFolderAsZip(folderId: string, allPages: Page[], folderName: string) {
|
|
const zip = new JSZip();
|
|
const cleanFolderName = sanitizeFilename(folderName);
|
|
const folder = zip.folder(cleanFolderName);
|
|
|
|
if (!folder) {
|
|
console.error("Failed to create zip folder");
|
|
return;
|
|
}
|
|
|
|
addPagesToZipRecursive(folder, folderId, allPages);
|
|
|
|
try {
|
|
const content = await zip.generateAsync({ type: "blob" });
|
|
saveAs(content, `${folderName}.zip`);
|
|
} catch (e) {
|
|
console.error("Failed to generate zip", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exports ALL pages as a Zip backup
|
|
*/
|
|
export async function exportAllPagesAsZip(allPages: Page[]) {
|
|
const zip = new JSZip();
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 16); // YYYY-MM-DDTHH-mm
|
|
const zipName = `noteai-backup-${timestamp}`;
|
|
|
|
// Root level pages (parentId is null)
|
|
addPagesToZipRecursive(zip, null, allPages);
|
|
|
|
try {
|
|
const content = await zip.generateAsync({ type: "blob" });
|
|
saveAs(content, `${zipName}.zip`);
|
|
} catch (e) {
|
|
console.error("Failed to generate backup zip", e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// Helper to recursively add pages to zip
|
|
function addPagesToZipRecursive(currentFolder: JSZip, parentId: string | null, allPages: Page[]) {
|
|
const children = allPages.filter(p => p.parentId === parentId);
|
|
|
|
children.forEach(page => {
|
|
const cleanTitle = sanitizeFilename(page.title);
|
|
|
|
if (page.type === 'folder') {
|
|
const subFolder = currentFolder.folder(cleanTitle);
|
|
if (subFolder) {
|
|
// Determine if we should look for children with this page's ID
|
|
addPagesToZipRecursive(subFolder, page.id, allPages);
|
|
}
|
|
} else {
|
|
const markdown = pageToMarkdown(page);
|
|
// Handle duplicate filenames by appending ID if necessary?
|
|
// For simplicity, we assume titles are unique enough or zip handles overwrite (last wins).
|
|
// Better: just append a counter if exists, but JSZip might overwrite.
|
|
currentFolder.file(`${cleanTitle}.md`, markdown);
|
|
}
|
|
});
|
|
}
|