Files
lms-sb/src/app/(student)/wrapped/_client/reader.ts
T
admins 06b8d9f64b Add Obsidian Wrapped feature (inside-LMS, client-side vault parsing)
- WrappedRun Prisma model + migration (aggregates only, no note names/text)
- /wrapped authed route: FSA + webkitdirectory + drag-drop reader, Web Worker parser
- archetype + score heuristics, ДС-2 result card
- POST /api/wrapped/run (userId from session), public /share/wrapped/[token]
- next/og OG image (Satori+resvg-wasm, alpine-safe) + Fira Mono ttf
- middleware: /share/wrapped public

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:19:59 +05:00

80 lines
3.0 KiB
TypeScript

// Чтение хранилища в браузере — три пути ввода, все → RawNote[].
// Содержимое заметок остаётся в браузере (приватность-моат).
import type { RawNote } from "./types";
const SKIP = ["/.obsidian/", "/.trash/", "/.git/"];
function isMd(path: string): boolean {
return path.toLowerCase().endsWith(".md") && !SKIP.some((s) => path.includes(s));
}
/* eslint-disable @typescript-eslint/no-explicit-any */
export function supportsFSA(): boolean {
return typeof window !== "undefined" && "showDirectoryPicker" in window;
}
// 1. File System Access API (Chromium: Chrome/Edge/Brave)
export async function readViaFSA(): Promise<RawNote[]> {
const dir: any = await (window as any).showDirectoryPicker();
const notes: RawNote[] = [];
async function walk(handle: any, prefix: string): Promise<void> {
for await (const [name, h] of handle.entries()) {
const path = prefix + "/" + name;
if (h.kind === "directory") {
if (name.startsWith(".")) continue;
await walk(h, path);
} else if (isMd(path)) {
const file: File = await h.getFile();
notes.push({ name, text: await file.text(), ctime: file.lastModified, mtime: file.lastModified });
}
}
}
await walk(dir, "");
return notes;
}
// 2. <input type="file" webkitdirectory> (Safari / Firefox)
export async function readViaInput(files: FileList): Promise<RawNote[]> {
const notes: RawNote[] = [];
for (const file of Array.from(files)) {
const path = (file as any).webkitRelativePath || file.name;
if (!isMd(path)) continue;
notes.push({ name: file.name, text: await file.text(), ctime: file.lastModified, mtime: file.lastModified });
}
return notes;
}
// 3. drag-drop папки (webkitGetAsEntry)
export async function readViaDrop(items: DataTransferItemList): Promise<RawNote[]> {
const notes: RawNote[] = [];
const roots: any[] = [];
for (const item of Array.from(items)) {
const entry = (item as any).webkitGetAsEntry?.();
if (entry) roots.push(entry);
}
async function readEntry(entry: any, prefix: string): Promise<void> {
const path = prefix + "/" + entry.name;
if (entry.isDirectory) {
if (entry.name.startsWith(".")) return;
const reader = entry.createReader();
const children: any[] = await new Promise((resolve) => {
const all: any[] = [];
const readBatch = () =>
reader.readEntries((batch: any[]) => {
if (!batch.length) return resolve(all);
all.push(...batch);
readBatch();
});
readBatch();
});
for (const c of children) await readEntry(c, path);
} else if (isMd(path)) {
const file: File = await new Promise((resolve, reject) => entry.file(resolve, reject));
notes.push({ name: entry.name, text: await file.text(), ctime: file.lastModified, mtime: file.lastModified });
}
}
for (const e of roots) await readEntry(e, "");
return notes;
}