项目文件夹

文件
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

144 行
4.5 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { IcFileText, IcNavBack } from '../res/icons';
import * as FileSystem from '@/os/FileSystemService';
import { useFileManagerGestures } from '../hooks/useFileManagerGestures';
import { strings } from '../res/strings';
import { stringsEn } from '../res/strings.en';
import { useAppStrings } from '@/os/useAppStrings';
type LoadState =
| { status: 'loading'; text: '' }
| { status: 'ready'; text: string }
| { status: 'error'; text: '' };
function decodePdfString(value: string): string {
return value
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\n')
.replace(/\\t/g, '\t')
.replace(/\\\(/g, '(')
.replace(/\\\)/g, ')')
.replace(/\\\\/g, '\\');
}
function extractSimplePdfText(raw: string): string {
const lines: string[] = [];
const textObjectPattern = /BT([\s\S]*?)ET/g;
let objectMatch: RegExpExecArray | null;
while ((objectMatch = textObjectPattern.exec(raw)) !== null) {
const body = objectMatch[1];
const stringPattern = /\((?:\\.|[^\\)])*\)\s*Tj/g;
let stringMatch: RegExpExecArray | null;
while ((stringMatch = stringPattern.exec(body)) !== null) {
const token = stringMatch[0];
const encoded = token.slice(1, token.lastIndexOf(')'));
lines.push(decodePdfString(encoded));
}
}
return lines.join('\n').trim();
}
export const PdfPreviewPage: React.FC = () => {
const [searchParams] = useSearchParams();
const { bindBack } = useFileManagerGestures();
const s = useAppStrings(strings, stringsEn);
const path = searchParams.get('path') || '';
const [loadState, setLoadState] = useState<LoadState>({ status: 'loading', text: '' });
const fileName = useMemo(() => {
if (!path) return s.pdf_preview_no_file;
const parts = path.split('/').filter(Boolean);
return parts[parts.length - 1] || s.pdf_preview_no_file;
}, [path, s.pdf_preview_no_file]);
const file = useMemo(() => (path ? FileSystem.getNode(path) : null), [path]);
useEffect(() => {
let cancelled = false;
async function loadPdf() {
if (!path || !file || file.type !== 'file') {
setLoadState({ status: 'error', text: '' });
return;
}
setLoadState({ status: 'loading', text: '' });
const blob = await FileSystem.readFile(path);
if (cancelled) return;
if (!blob) {
setLoadState({ status: 'error', text: '' });
return;
}
const raw = await blob.text();
if (cancelled) return;
const text = extractSimplePdfText(raw);
if (!text) {
setLoadState({ status: 'error', text: '' });
return;
}
setLoadState({ status: 'ready', text });
}
loadPdf();
return () => {
cancelled = true;
};
}, [file, path]);
return (
<div className="h-full bg-app-surface flex flex-col">
<div className="pt-10 px-4 pb-3 flex items-center gap-3 border-b border-gray-100 shrink-0">
<button
{...bindBack()}
className="w-10 h-10 flex items-center justify-center -ml-2 active:opacity-60"
>
<IcNavBack size={28} className="text-app-text" />
</button>
<div className="w-10 h-10 rounded-lg bg-red-50 flex items-center justify-center shrink-0">
<IcFileText size={24} className="text-red-500" />
</div>
<div className="flex-1 min-w-0">
<h1 className="text-[17px] font-semibold text-app-text truncate">{fileName}</h1>
{file && (
<div className="text-[12px] text-gray-500 truncate">
{FileSystem.formatFileSize(file.size)}
</div>
)}
</div>
</div>
<div
className="flex-1 bg-white overflow-y-auto"
data-scroll-container="main"
data-scroll-direction="vertical"
>
{loadState.status === 'loading' && (
<div className="h-full flex items-center justify-center text-[14px] text-gray-400">
{s.pdf_preview_loading}
</div>
)}
{loadState.status === 'error' && (
<div className="h-full flex items-center justify-center px-8 text-center text-[14px] text-gray-400">
{s.pdf_preview_failed}
</div>
)}
{loadState.status === 'ready' && (
<div className="min-h-full bg-white px-7 py-8 text-[15px] leading-7 text-gray-900 whitespace-pre-wrap break-words">
{loadState.text}
</div>
)}
</div>
</div>
);
};
export default PdfPreviewPage;