114 lines
4.8 KiB
TypeScript
114 lines
4.8 KiB
TypeScript
import { lazy, Suspense, useCallback, useState } from 'react';
|
|
import { channelPath, navigate, sharePath } from '@/app/navigation';
|
|
import { buildInfo } from '@/lib/buildInfo';
|
|
import type { ChannelInfo, Share } from '@/core-bridge/types';
|
|
import type { ClipboardPayload } from '@/lib/clipboard';
|
|
import { useGlobalPaste } from '@/lib/useGlobalPaste';
|
|
import { useChannels } from '@/store/channels';
|
|
import { useShares } from '@/store/shares';
|
|
import { solveCreationCaptcha } from '@/core-bridge/captcha';
|
|
import { errorMessage } from '@/core-bridge/errors';
|
|
import { Button } from '@/ui/Button';
|
|
import { ThemeMenu } from '@/ui/ThemeMenu';
|
|
import { ChannelList } from './ChannelList';
|
|
import { CreateSheet } from './CreateSheet';
|
|
import { QuickShare } from './QuickShare';
|
|
import styles from './Home.module.css';
|
|
|
|
const ScanSheet = lazy(() => import('@/features/scan/ScanSheet').then((m) => ({ default: m.ScanSheet })));
|
|
|
|
/** 初始状态:什么都没创建时看到的页面。核心是「快速分享」 */
|
|
export function Home() {
|
|
const createDraft = useShares((s) => s.createDraft);
|
|
const loadDemo = useChannels((s) => s.loadDemo);
|
|
const loadShareDemo = useShares((s) => s.loadDemo);
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
const [scanOpen, setScanOpen] = useState(false);
|
|
const [busy, setBusy] = useState(false);
|
|
const [failedMessage, setFailedMessage] = useState('');
|
|
const [shareText, setShareText] = useState('');
|
|
const [shareFiles, setShareFiles] = useState<File[]>([]);
|
|
|
|
const open = useCallback((c: ChannelInfo | Share) => navigate('state' in c ? sharePath(c.id) : channelPath(c.id)), []);
|
|
|
|
const share = useCallback(
|
|
async (p: ClipboardPayload) => {
|
|
if (busy || (p.files.length === 0 && !p.text.trim())) return false;
|
|
setBusy(true);
|
|
setFailedMessage('');
|
|
try {
|
|
const captchaToken = await solveCreationCaptcha();
|
|
open(await createDraft(p, captchaToken));
|
|
setShareFiles([]);
|
|
setShareText('');
|
|
return true;
|
|
} catch (error) {
|
|
setFailedMessage(errorMessage(error));
|
|
return false;
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
},
|
|
[busy, createDraft, open],
|
|
);
|
|
|
|
const onClipboard = useCallback((p: ClipboardPayload) => {
|
|
if (p.files.length > 0) setShareFiles((files) => [...files, ...p.files]);
|
|
if (p.text) setShareText((text) => text ? `${text}\n${p.text}` : p.text);
|
|
}, []);
|
|
useGlobalPaste(onClipboard);
|
|
|
|
return (
|
|
<div className={styles.page}>
|
|
<div className={styles.appearance}><ThemeMenu /></div>
|
|
<main className={styles.main}>
|
|
<header className={styles.head}>
|
|
<h1 className={styles.logo}>WebDrop</h1>
|
|
<p className={styles.sub}>在自己的设备之间快速传文件和文本,也可以分享给别人。</p>
|
|
</header>
|
|
|
|
<QuickShare
|
|
busy={busy}
|
|
text={shareText}
|
|
files={shareFiles}
|
|
onTextChange={setShareText}
|
|
onFilesChange={setShareFiles}
|
|
onShare={share}
|
|
/>
|
|
{failedMessage ? <p role="alert" className={styles.error}>创建分享失败:{failedMessage}</p> : null}
|
|
|
|
<ChannelList kind="share" title="我的分享" onOpen={open} />
|
|
|
|
<section className={styles.channels} aria-labelledby="channels-title">
|
|
<div className={styles.sectionHead}>
|
|
<h2 id="channels-title">设备频道</h2>
|
|
<p>多台自己的设备之间长期同步</p>
|
|
</div>
|
|
<div className={styles.actions}>
|
|
<Button icon="edit" onClick={() => setCreateOpen(true)}>新建频道</Button>
|
|
<Button icon="scan" onClick={() => setScanOpen(true)}>加入频道</Button>
|
|
</div>
|
|
|
|
<ChannelList kind="channel" title="我的频道" onOpen={open} />
|
|
<p className={styles.note}>加密频道的密钥只在链接里,服务器看不到内容。持有链接的人可以读取该频道的全部历史。</p>
|
|
</section>
|
|
|
|
{import.meta.env.DEV ? (
|
|
<Button variant="ghost" onClick={() => { loadDemo(); loadShareDemo(); }}>载入演示数据</Button>
|
|
) : null}
|
|
</main>
|
|
|
|
<aside className={styles.buildInfo} aria-label="构建信息">
|
|
<span>前端 v{buildInfo.frontendVersion} · tag {buildInfo.frontendTag || '未标记'} · commit {buildInfo.frontendCommit}</span>
|
|
<span>后端 tag {buildInfo.backendTag || '未标记'} · commit {buildInfo.backendCommit}</span>
|
|
<time dateTime={buildInfo.builtAt}>构建时间 {buildInfo.builtAtShanghai}</time>
|
|
</aside>
|
|
|
|
{createOpen ? <CreateSheet onClose={() => setCreateOpen(false)} onCreated={open} /> : null}
|
|
<Suspense fallback={null}>
|
|
{scanOpen ? <ScanSheet onClose={() => setScanOpen(false)} onChannel={open} onShare={open} /> : null}
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|