feat(web): connect quick share flow
Signed-off-by: JustMe <mr.toing@outlook.com>
This commit is contained in:
1 parent
53768a195d
commit
e02e024605
138 files changed
+10614
No files matched your search
@@ -0,0 +1,209 @@
|
||||
import { lazy, Suspense, useCallback, useEffect, useRef, useState, type DragEvent } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { channelPath, navigate, sharePath } from '@/app/navigation';
|
||||
import { useDownload } from '@/features/item/download';
|
||||
import { isFiltering, matchesFilter, NO_FILTER } from '@/lib/filter';
|
||||
import type { ClipboardPayload } from '@/lib/clipboard';
|
||||
import { useGlobalPaste } from '@/lib/useGlobalPaste';
|
||||
import { useMediaQuery } from '@/lib/useMediaQuery';
|
||||
import { useChannels } from '@/store/channels';
|
||||
import { useWorkspace } from '@/store/workspace';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Sheet } from '@/ui/Sheet';
|
||||
import { ActionBar } from './ActionBar';
|
||||
import { ComposeSheet } from './ComposeSheet';
|
||||
import { DevicePanel } from './DevicePanel';
|
||||
import { FilterBar } from './FilterBar';
|
||||
import { ItemList } from './ItemList';
|
||||
import { LatestCard } from './LatestCard';
|
||||
import { Notepad } from './Notepad';
|
||||
import { SideRail } from './SideRail';
|
||||
import { TopBar } from './TopBar';
|
||||
import { UndoBar } from './UndoBar';
|
||||
import styles from './Workspace.module.css';
|
||||
|
||||
const PreviewSheet = lazy(() => import('@/features/item/PreviewSheet'));
|
||||
const PreviewPane = lazy(() => import('@/features/item/PreviewPane'));
|
||||
const ScanSheet = lazy(() => import('@/features/scan/ScanSheet').then((m) => ({ default: m.ScanSheet })));
|
||||
|
||||
const UNDO_MS = 5000;
|
||||
|
||||
export function Workspace() {
|
||||
const desktop = useMediaQuery('(width >= 900px) and (height >= 500px)');
|
||||
const sendFiles = useWorkspace((s) => s.sendFiles);
|
||||
const sendText = useWorkspace((s) => s.sendText);
|
||||
const remove = useWorkspace((s) => s.remove);
|
||||
const download = useDownload();
|
||||
|
||||
// 刚创建的频道:直接展示分享面板和二维码,让对方马上能扫
|
||||
const [justCreated] = useState(() => useChannels.getState().justCreatedId !== null);
|
||||
const [panelOpen, setPanelOpen] = useState(justCreated); // 手机:设备与分享抽屉
|
||||
const [railOpen, setRailOpen] = useState(justCreated); // 电脑:右侧栏展开
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [filter, setFilter] = useState(NO_FILTER);
|
||||
const [selectedId, setSelectedId] = useState<string>();
|
||||
const [sheetId, setSheetId] = useState<string>();
|
||||
const [checked, setChecked] = useState<ReadonlySet<string>>(new Set());
|
||||
const [composeOpen, setComposeOpen] = useState(false);
|
||||
const [scanOpen, setScanOpen] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [pending, setPending] = useState<string>();
|
||||
const pendingRef = useRef<{ id: string; timer: number }>(undefined);
|
||||
|
||||
const sendPayload = useCallback(
|
||||
(p: ClipboardPayload) => {
|
||||
if (p.files.length > 0) void sendFiles(p.files);
|
||||
else if (p.text) void sendText(p.text);
|
||||
},
|
||||
[sendFiles, sendText],
|
||||
);
|
||||
useGlobalPaste(sendPayload);
|
||||
|
||||
// 从首页「快速分享」带过来的内容,进入频道后发送
|
||||
useEffect(() => {
|
||||
const { consumePayload, clearJustCreated } = useChannels.getState();
|
||||
const p = consumePayload();
|
||||
if (p) sendPayload(p);
|
||||
clearJustCreated();
|
||||
}, [sendPayload]);
|
||||
|
||||
const ids = useWorkspace(
|
||||
useShallow((s) =>
|
||||
s.order.filter((id) => {
|
||||
const item = s.byId[id];
|
||||
return id !== pending && item !== undefined && matchesFilter(item, filter);
|
||||
}),
|
||||
),
|
||||
);
|
||||
const filtering = isFiltering(filter);
|
||||
const latestId = !desktop && !filtering ? ids[0] : undefined;
|
||||
const listIds = latestId ? ids.slice(1) : ids;
|
||||
const activeId = selectedId && ids.includes(selectedId) ? selectedId : ids[0];
|
||||
|
||||
const closeSheet = useCallback(() => setSheetId(undefined), []);
|
||||
const closePanel = useCallback(() => setPanelOpen(false), []);
|
||||
const closeCompose = useCallback(() => setComposeOpen(false), []);
|
||||
const closeScan = useCallback(() => setScanOpen(false), []);
|
||||
|
||||
function requestDelete(id: string) {
|
||||
if (pendingRef.current) {
|
||||
clearTimeout(pendingRef.current.timer);
|
||||
void remove(pendingRef.current.id);
|
||||
}
|
||||
const timer = window.setTimeout(() => {
|
||||
void remove(id);
|
||||
pendingRef.current = undefined;
|
||||
setPending(undefined);
|
||||
}, UNDO_MS);
|
||||
pendingRef.current = { id, timer };
|
||||
setPending(id);
|
||||
}
|
||||
|
||||
function undoDelete() {
|
||||
if (!pendingRef.current) return;
|
||||
clearTimeout(pendingRef.current.timer);
|
||||
pendingRef.current = undefined;
|
||||
setPending(undefined);
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
if (searchOpen) setFilter((f) => ({ ...f, query: '' }));
|
||||
setSearchOpen((v) => !v);
|
||||
}
|
||||
|
||||
function toggleChecked(id: string) {
|
||||
setChecked((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (!next.delete(id)) next.add(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function downloadChecked() {
|
||||
const { byId } = useWorkspace.getState();
|
||||
for (const id of checked) {
|
||||
const item = byId[id];
|
||||
if (item) void download(item);
|
||||
}
|
||||
setChecked(new Set());
|
||||
}
|
||||
|
||||
function onDragOver(e: DragEvent) {
|
||||
if (!e.dataTransfer.types.includes('Files')) return;
|
||||
e.preventDefault();
|
||||
setDragging(true);
|
||||
}
|
||||
|
||||
function onDrop(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
const files = [...e.dataTransfer.files];
|
||||
if (files.length) void sendFiles(files);
|
||||
}
|
||||
|
||||
const emptyText = filtering ? '没有符合条件的内容。' : '还没有内容。粘贴、拖入文件,或在其他设备上打开此频道。';
|
||||
const topBar = (
|
||||
<TopBar
|
||||
onBack={() => navigate('/')}
|
||||
onOpenPanel={desktop ? undefined : () => setPanelOpen(true)}
|
||||
searchOpen={searchOpen}
|
||||
filtering={filtering}
|
||||
onToggleSearch={toggleSearch}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={desktop ? styles.desktop : styles.mobile} onDragOver={onDragOver} onDragLeave={() => setDragging(false)} onDrop={onDrop}>
|
||||
{desktop ? (
|
||||
<>
|
||||
{topBar}
|
||||
<div className={styles.list}>
|
||||
<ActionBar layout="toolbar" onFiles={(f) => void sendFiles(f)} onText={(t) => void sendText(t)} onCompose={() => setComposeOpen(true)} onScan={() => setScanOpen(true)} />
|
||||
<Notepad />
|
||||
<FilterBar filter={filter} searchOpen={searchOpen} onChange={setFilter} />
|
||||
{checked.size > 0 ? (
|
||||
<div className={styles.batch}>
|
||||
<span>已选 {checked.size} 项</span>
|
||||
<span>
|
||||
<Button variant="primary" icon="download" onClick={downloadChecked}>下载</Button>
|
||||
<Button variant="ghost" onClick={() => setChecked(new Set())}>取消</Button>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.scroll}>
|
||||
<ItemList ids={ids} selectedId={activeId} checked={checked} empty={emptyText} onSelect={setSelectedId} onToggle={toggleChecked} />
|
||||
</div>
|
||||
</div>
|
||||
<Suspense fallback={null}>
|
||||
<PreviewPane id={activeId} onDelete={requestDelete} />
|
||||
</Suspense>
|
||||
<SideRail open={railOpen} qrOpen={justCreated} onToggle={() => setRailOpen((v) => !v)} />
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.column}>
|
||||
{topBar}
|
||||
{latestId ? <LatestCard id={latestId} onOpen={setSheetId} /> : null}
|
||||
<Notepad />
|
||||
<FilterBar filter={filter} searchOpen={searchOpen} onChange={setFilter} />
|
||||
<div className={styles.grow}>
|
||||
<ItemList ids={listIds} empty={emptyText} onSelect={setSheetId} />
|
||||
</div>
|
||||
<ActionBar layout="dock" onFiles={(f) => void sendFiles(f)} onText={(t) => void sendText(t)} onCompose={() => setComposeOpen(true)} onScan={() => setScanOpen(true)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!desktop && panelOpen ? (
|
||||
<Sheet title="设备与分享" onClose={closePanel}>
|
||||
<DevicePanel qrOpen={justCreated} />
|
||||
</Sheet>
|
||||
) : null}
|
||||
<Suspense fallback={null}>
|
||||
{!desktop && sheetId ? <PreviewSheet id={sheetId} onClose={closeSheet} onDelete={requestDelete} /> : null}
|
||||
{scanOpen ? <ScanSheet onClose={closeScan} onChannel={(c) => navigate(channelPath(c.id))} onShare={(sh) => navigate(sharePath(sh.id))} /> : null}
|
||||
</Suspense>
|
||||
{composeOpen ? <ComposeSheet onClose={closeCompose} onSubmit={(t) => void sendText(t)} /> : null}
|
||||
{pending ? <UndoBar onUndo={undoDelete} /> : null}
|
||||
{dragging ? <div className={styles.drop}>松开以发送文件</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user