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,191 @@
|
||||
import { previewKindOf } from '@/lib/fileKind';
|
||||
import type { ShareApi } from './shareApi';
|
||||
import { b64url } from './mockLinks';
|
||||
import { PHONE, archiveEntries, makeSvgBlob, seedItems } from './mockSeed';
|
||||
import type { FileItem, Item, Share, ShareEvent, ShareSettings } from './types';
|
||||
|
||||
const wait = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||||
const CREATOR = 'creator';
|
||||
export const DEMO_SHARE_ID = 'ShareDemoShareDemoAbcd';
|
||||
const DEMO_KEY = 'Zm9vYmFyYmF6cXV4Zm9vYmFyYmF6cXV4Zm9vYmFyYmF';
|
||||
|
||||
interface Runtime {
|
||||
share: Share;
|
||||
items: Map<string, Item>;
|
||||
blobs: Map<string, Blob | Promise<Blob>>;
|
||||
}
|
||||
|
||||
/** 便捷分享的假实现:不联网,数据都在内存里,刷新即丢。接口见 shareApi.ts */
|
||||
export function createMockShareApi(): ShareApi {
|
||||
const runtimes = new Map<string, Runtime>();
|
||||
let current: Runtime | undefined;
|
||||
let emit: (e: ShareEvent) => void = () => {};
|
||||
const timers = new Set<number>();
|
||||
|
||||
const uid = () => `it-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const put = (item: Item) => {
|
||||
current?.items.set(item.id, item);
|
||||
emit({ t: 'item', item });
|
||||
};
|
||||
const draftOnly = () => {
|
||||
if (!current || current.share.state !== 'draft') throw new Error('forbidden');
|
||||
return current;
|
||||
};
|
||||
|
||||
function seedDemo(): Runtime {
|
||||
const share: Share = {
|
||||
id: DEMO_SHARE_ID,
|
||||
state: 'sealed',
|
||||
mine: false,
|
||||
link: `${location.origin}/s/${DEMO_SHARE_ID}#k=${DEMO_KEY}`,
|
||||
createdAt: Date.now() - 2 * 3600_000,
|
||||
ttlSeconds: 86400,
|
||||
seats: 10,
|
||||
maxDownloads: 5,
|
||||
};
|
||||
const rt: Runtime = { share, items: new Map(), blobs: new Map() };
|
||||
const keep = new Set(['it-zip', 'it-image', 'it-json', 'it-short', 'it-pdf']);
|
||||
for (const item of seedItems().items) {
|
||||
if (!keep.has(item.id)) continue;
|
||||
rt.items.set(item.id, item.kind === 'file'
|
||||
? { ...item, deviceId: CREATOR, versions: undefined, unseen: false, remaining: share.maxDownloads }
|
||||
: { ...item, deviceId: CREATOR });
|
||||
}
|
||||
rt.blobs.set('it-image', makeSvgBlob());
|
||||
rt.blobs.set('it-zip', new Blob(['mock zip'], { type: 'application/zip' }));
|
||||
rt.blobs.set('it-pdf', new Blob(['%PDF-1.4 mock'], { type: 'application/pdf' }));
|
||||
runtimes.set(share.id, rt);
|
||||
return rt;
|
||||
}
|
||||
|
||||
return {
|
||||
async createDraft(settings) {
|
||||
await wait(300);
|
||||
const id = b64url(16);
|
||||
const share: Share = {
|
||||
id,
|
||||
state: 'draft',
|
||||
mine: true,
|
||||
link: `${location.origin}/s/${id}#k=${b64url(32)}`,
|
||||
createdAt: Date.now(),
|
||||
...settings,
|
||||
};
|
||||
runtimes.set(id, { share, items: new Map(), blobs: new Map() });
|
||||
return share;
|
||||
},
|
||||
|
||||
async updateSettings(id, patch: Partial<ShareSettings>) {
|
||||
const rt = runtimes.get(id);
|
||||
if (!rt || rt.share.state !== 'draft') throw new Error('forbidden');
|
||||
rt.share = { ...rt.share, ...patch };
|
||||
emit({ t: 'share', share: rt.share });
|
||||
return rt.share;
|
||||
},
|
||||
|
||||
async seal(id) {
|
||||
await wait(300);
|
||||
const rt = runtimes.get(id);
|
||||
if (!rt || rt.share.state !== 'draft') throw new Error('forbidden');
|
||||
// 丢弃所有者密钥:此后没有人能改,创建者也只是一个读者
|
||||
rt.share = { ...rt.share, state: 'sealed' };
|
||||
for (const item of rt.items.values()) {
|
||||
if (item.kind === 'file') rt.items.set(item.id, { ...item, remaining: rt.share.maxDownloads });
|
||||
}
|
||||
emit({ t: 'share', share: rt.share });
|
||||
for (const item of rt.items.values()) emit({ t: 'item', item });
|
||||
return rt.share;
|
||||
},
|
||||
|
||||
async join(link) {
|
||||
await wait(300);
|
||||
const known = runtimes.get(link.id);
|
||||
if (known) return known.share;
|
||||
// 原型里不认识的链接:当作一个已封存、没有内容的分享
|
||||
const share: Share = {
|
||||
id: link.id,
|
||||
state: 'sealed',
|
||||
mine: false,
|
||||
link: `${location.origin}/s/${link.id}#k=${link.key}`,
|
||||
createdAt: Date.now(),
|
||||
ttlSeconds: 86400,
|
||||
seats: 10,
|
||||
maxDownloads: null,
|
||||
};
|
||||
runtimes.set(link.id, { share, items: new Map(), blobs: new Map() });
|
||||
return share;
|
||||
},
|
||||
|
||||
loadDemo() {
|
||||
return [(runtimes.get(DEMO_SHARE_ID) ?? seedDemo()).share];
|
||||
},
|
||||
|
||||
connect(shareId, onEvent) {
|
||||
emit = onEvent;
|
||||
current = runtimes.get(shareId);
|
||||
const rt = current;
|
||||
if (!rt) return () => {};
|
||||
const id = window.setTimeout(() => {
|
||||
timers.delete(id);
|
||||
emit({ t: 'share', share: rt.share });
|
||||
for (const item of rt.items.values()) emit({ t: 'item', item });
|
||||
emit({ t: 'ready' });
|
||||
}, 400);
|
||||
timers.add(id);
|
||||
return () => {
|
||||
for (const t of timers) clearTimeout(t);
|
||||
timers.clear();
|
||||
emit = () => {};
|
||||
};
|
||||
},
|
||||
|
||||
async addText(text) {
|
||||
draftOnly();
|
||||
put({ id: uid(), kind: 'text', ts: Date.now(), deviceId: PHONE, state: { kind: 'complete' }, text });
|
||||
},
|
||||
|
||||
async addFiles(files) {
|
||||
const rt = draftOnly();
|
||||
for (const file of files) {
|
||||
const id = uid();
|
||||
const base: FileItem = {
|
||||
id, kind: 'file', ts: Date.now(), deviceId: PHONE, state: { kind: 'receiving', progress: 0 },
|
||||
name: file.name, mime: file.type, size: file.size, remaining: null,
|
||||
preview: previewKindOf(file.type, file.name),
|
||||
};
|
||||
rt.blobs.set(id, file);
|
||||
put(base);
|
||||
for (let p = 0.25; p < 1; p += 0.25) {
|
||||
await wait(200);
|
||||
put({ ...base, state: { kind: 'receiving', progress: p } });
|
||||
}
|
||||
await wait(200);
|
||||
put({ ...base, state: { kind: 'complete' } });
|
||||
}
|
||||
},
|
||||
|
||||
async removeItem(id) {
|
||||
const rt = draftOnly();
|
||||
rt.items.delete(id);
|
||||
rt.blobs.delete(id);
|
||||
emit({ t: 'removed', id });
|
||||
},
|
||||
|
||||
async getBlob(id) {
|
||||
const item = current?.items.get(id);
|
||||
// 封存后每次拉取消耗一次下载(GATED,api.md);用完即 410
|
||||
if (item?.kind === 'file' && current?.share.state === 'sealed' && item.remaining !== null) {
|
||||
if (item.remaining <= 0) throw new Error('gone');
|
||||
put({ ...item, remaining: item.remaining - 1 });
|
||||
}
|
||||
const b = current?.blobs.get(id);
|
||||
if (b) return b;
|
||||
if (item?.kind === 'text') return new Blob([item.text], { type: 'text/plain' });
|
||||
throw new Error('not found');
|
||||
},
|
||||
|
||||
async listArchive() {
|
||||
await wait(250);
|
||||
return archiveEntries;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user