feat: integrate client and server sharing workflows

This commit is contained in:
justme committed 2026-10-02 10:49:45 +08:00
1 parent 42b93ae2d8
commit 7406e2fe37
101 files changed
+3153 -253

No files matched your search

+40 -36
View File
@@ -1,11 +1,11 @@
import { lazy, Suspense, useCallback, useState, type DragEvent } from 'react';
import { lazy, Suspense, useCallback, useState } from 'react';
import { channelPath, navigate, sharePath } from '@/app/navigation';
import type { ChannelInfo, Share } from '@/core-bridge/types';
import { ComposeSheet } from '@/features/channel/ComposeSheet';
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 { Button } from '@/ui/Button';
import { ChannelList } from './ChannelList';
import { CreateSheet } from './CreateSheet';
@@ -21,67 +21,74 @@ export function Home() {
const loadShareDemo = useShares((s) => s.loadDemo);
const [createOpen, setCreateOpen] = useState(false);
const [scanOpen, setScanOpen] = useState(false);
const [composeOpen, setComposeOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [dragging, setDragging] = useState(false);
const [failed, setFailed] = useState(false);
const [shareText, setShareText] = useState('');
const [shareFiles, setShareFiles] = useState<File[]>([]);
const open = (c: ChannelInfo | Share) => navigate('state' in c ? sharePath(c.id) : channelPath(c.id));
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)) return;
if (busy || (p.files.length === 0 && !p.text.trim())) return false;
setBusy(true);
setFailed(false);
try {
open(await createDraft(p));
const captchaToken = await solveCreationCaptcha();
open(await createDraft(p, captchaToken));
setShareFiles([]);
setShareText('');
return true;
} catch {
setFailed(true);
return false;
} finally {
setBusy(false);
}
},
[busy, createDraft],
[busy, createDraft, open],
);
useGlobalPaste(share);
function onDragOver(e: DragEvent) {
if (!e.dataTransfer.types.includes('Files')) return;
e.preventDefault();
setDragging(true);
}
function onDrop(e: DragEvent) {
e.preventDefault();
setDragging(false);
void share({ files: [...e.dataTransfer.files], text: '' });
}
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} onDragOver={onDragOver} onDragLeave={() => setDragging(false)} onDrop={onDrop}>
<div className={styles.page}>
<main className={styles.main}>
<header className={styles.head}>
<h1 className={styles.logo}>WebDrop</h1>
<p className={styles.sub}>在自己的设备之间快速传文件和文本,也可以分享给别人。</p>
</header>
<QuickShare busy={busy} dragging={dragging} onShare={share} onCompose={() => setComposeOpen(true)} />
<QuickShare
busy={busy}
text={shareText}
files={shareFiles}
onTextChange={setShareText}
onFilesChange={setShareFiles}
onShare={share}
/>
{failed ? <p role="alert" className={styles.error}>创建分享失败,请重试。</p> : null}
<ChannelList kind="share" title="我的分享" onOpen={open} />
<div className={styles.divider}>常驻频道:多台自己的设备之间同步</div>
<div className={styles.actions}>
<Button icon="edit" onClick={() => setCreateOpen(true)}>新建频道</Button>
<Button icon="scan" onClick={() => setScanOpen(true)}>加入频道</Button>
</div>
<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} />
<ChannelList kind="channel" title="我的频道" onOpen={open} />
<p className={styles.note}>加密频道的密钥只在链接里,服务器看不到内容。持有链接的人可以读取该频道的全部历史。</p>
</section>
<p className={styles.note}>
加密频道的密钥只在链接里,服务器看不到内容。持有链接的人可以读取该频道的全部历史。
</p>
{import.meta.env.DEV ? (
<Button variant="ghost" onClick={() => { loadDemo(); loadShareDemo(); }}>载入演示数据</Button>
) : null}
@@ -89,11 +96,8 @@ export function Home() {
{createOpen ? <CreateSheet onClose={() => setCreateOpen(false)} onCreated={open} /> : null}
<Suspense fallback={null}>
{scanOpen ? <ScanSheet onClose={() => setScanOpen(false)} onJoined={open} /> : null}
{scanOpen ? <ScanSheet onClose={() => setScanOpen(false)} onChannel={open} onShare={open} /> : null}
</Suspense>
{composeOpen ? (
<ComposeSheet onClose={() => setComposeOpen(false)} onSubmit={(text) => void share({ files: [], text })} />
) : null}
</div>
);
}