feat(editor): add resource controller (#12121)
Closes: [BS-3398](https://linear.app/affine-design/issue/BS-3398/实现资源控制器) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Introduced a ResourceController for centralized and reactive management of attachment resource states, improving error handling, loading indicators, and UI state resolution for attachments. - Added public access to resource management utilities via new export paths. - **Refactor** - Streamlined attachment state management by replacing manual state tracking with the new ResourceController, simplifying code and enhancing maintainability. - Updated rendering logic for attachments to use unified state objects for clearer UI feedback. - Centralized blob URL creation and download state management within the ResourceController. - **Chores** - Updated dependencies and internal references to reflect the new resource management approach, ensuring consistency across packages. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -258,6 +258,7 @@
|
|||||||
"./components/toolbar": "./src/components/toolbar.ts",
|
"./components/toolbar": "./src/components/toolbar.ts",
|
||||||
"./components/view-dropdown-menu": "./src/components/view-dropdown-menu.ts",
|
"./components/view-dropdown-menu": "./src/components/view-dropdown-menu.ts",
|
||||||
"./components/tooltip-content-with-shortcut": "./src/components/tooltip-content-with-shortcut.ts",
|
"./components/tooltip-content-with-shortcut": "./src/components/tooltip-content-with-shortcut.ts",
|
||||||
|
"./components/resource": "./src/components/resource.ts",
|
||||||
"./rich-text": "./src/rich-text/index.ts",
|
"./rich-text": "./src/rich-text/index.ts",
|
||||||
"./rich-text/effects": "./src/rich-text/effects.ts",
|
"./rich-text/effects": "./src/rich-text/effects.ts",
|
||||||
"./shared/adapters": "./src/shared/adapters.ts",
|
"./shared/adapters": "./src/shared/adapters.ts",
|
||||||
|
|||||||
1
blocksuite/affine/all/src/components/resource.ts
Normal file
1
blocksuite/affine/all/src/components/resource.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from '@blocksuite/affine-components/resource';
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
"@blocksuite/icons": "^2.2.12",
|
"@blocksuite/icons": "^2.2.12",
|
||||||
"@blocksuite/std": "workspace:*",
|
"@blocksuite/std": "workspace:*",
|
||||||
"@blocksuite/store": "workspace:*",
|
"@blocksuite/store": "workspace:*",
|
||||||
"@blocksuite/sync": "workspace:*",
|
|
||||||
"@floating-ui/dom": "^1.6.13",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@preact/signals-core": "^1.8.0",
|
"@preact/signals-core": "^1.8.0",
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
getLoadingIconWith,
|
getLoadingIconWith,
|
||||||
} from '@blocksuite/affine-components/icons';
|
} from '@blocksuite/affine-components/icons';
|
||||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||||
|
import {
|
||||||
|
type ResolvedStateInfo,
|
||||||
|
ResourceController,
|
||||||
|
} from '@blocksuite/affine-components/resource';
|
||||||
import { toast } from '@blocksuite/affine-components/toast';
|
import { toast } from '@blocksuite/affine-components/toast';
|
||||||
import {
|
import {
|
||||||
type AttachmentBlockModel,
|
type AttachmentBlockModel,
|
||||||
@@ -25,8 +29,7 @@ import {
|
|||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
import { BlockSelection } from '@blocksuite/std';
|
import { BlockSelection } from '@blocksuite/std';
|
||||||
import { Slice } from '@blocksuite/store';
|
import { Slice } from '@blocksuite/store';
|
||||||
import { type BlobState } from '@blocksuite/sync';
|
import { computed } from '@preact/signals-core';
|
||||||
import { effect, signal } from '@preact/signals-core';
|
|
||||||
import { html, type TemplateResult } from 'lit';
|
import { html, type TemplateResult } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
import { choose } from 'lit/directives/choose.js';
|
import { choose } from 'lit/directives/choose.js';
|
||||||
@@ -38,8 +41,6 @@ import { AttachmentEmbedProvider } from './embed';
|
|||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
import { downloadAttachmentBlob, refreshData } from './utils';
|
import { downloadAttachmentBlob, refreshData } from './utils';
|
||||||
|
|
||||||
type State = 'loading' | 'uploading' | 'warning' | 'oversize' | 'none';
|
|
||||||
|
|
||||||
@Peekable({
|
@Peekable({
|
||||||
enableOn: ({ model }: AttachmentBlockComponent) => {
|
enableOn: ({ model }: AttachmentBlockComponent) => {
|
||||||
return !model.doc.readonly && model.props.type.endsWith('pdf');
|
return !model.doc.readonly && model.props.type.endsWith('pdf');
|
||||||
@@ -50,7 +51,9 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
|||||||
|
|
||||||
blockDraggable = true;
|
blockDraggable = true;
|
||||||
|
|
||||||
blobState$ = signal<Partial<BlobState>>({});
|
resourceController = new ResourceController(
|
||||||
|
computed(() => this.model.props.sourceId$.value)
|
||||||
|
);
|
||||||
|
|
||||||
protected containerStyleMap = styleMap({
|
protected containerStyleMap = styleMap({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -98,24 +101,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
|||||||
};
|
};
|
||||||
|
|
||||||
refreshData = () => {
|
refreshData = () => {
|
||||||
refreshData(this.std, this).catch(console.error);
|
refreshData(this).catch(console.error);
|
||||||
};
|
|
||||||
|
|
||||||
updateBlobState(state: Partial<BlobState>) {
|
|
||||||
this.blobState$.value = { ...this.blobState$.value, ...state };
|
|
||||||
}
|
|
||||||
|
|
||||||
determineState = (
|
|
||||||
downloading: boolean,
|
|
||||||
uploading: boolean,
|
|
||||||
overSize: boolean,
|
|
||||||
error: boolean
|
|
||||||
): State => {
|
|
||||||
if (overSize) return 'oversize';
|
|
||||||
if (error) return 'warning';
|
|
||||||
if (uploading) return 'uploading';
|
|
||||||
if (downloading) return 'loading';
|
|
||||||
return 'none';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
protected get embedView() {
|
protected get embedView() {
|
||||||
@@ -137,29 +123,13 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
|||||||
|
|
||||||
this.contentEditable = 'false';
|
this.contentEditable = 'false';
|
||||||
|
|
||||||
|
// This is a tradeoff, initializing `Blob Sync Engine`.
|
||||||
|
this.resourceController.setEngine(this.std.store.blobSync);
|
||||||
|
|
||||||
|
this.disposables.add(this.resourceController.subscribe());
|
||||||
|
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
|
|
||||||
this.disposables.add(
|
|
||||||
effect(() => {
|
|
||||||
const blobId = this.model.props.sourceId$.value;
|
|
||||||
if (!blobId) return;
|
|
||||||
|
|
||||||
const blobState$ = this.std.store.blobSync.blobState$(blobId);
|
|
||||||
if (!blobState$) return;
|
|
||||||
|
|
||||||
const subscription = blobState$.subscribe(state => {
|
|
||||||
if (state.overSize || state.errorMessage) {
|
|
||||||
state.uploading = false;
|
|
||||||
state.downloading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateBlobState(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => subscription.unsubscribe();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!this.model.props.style && !this.doc.readonly) {
|
if (!this.model.props.style && !this.doc.readonly) {
|
||||||
this.doc.withoutTransact(() => {
|
this.doc.withoutTransact(() => {
|
||||||
this.doc.updateBlock(this.model, {
|
this.doc.updateBlock(this.model, {
|
||||||
@@ -230,11 +200,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
|||||||
|
|
||||||
protected renderWithHorizontal(
|
protected renderWithHorizontal(
|
||||||
classInfo: ClassInfo,
|
classInfo: ClassInfo,
|
||||||
icon: TemplateResult,
|
{ icon, title, description, state }: ResolvedStateInfo,
|
||||||
title: string,
|
kind: TemplateResult
|
||||||
description: string,
|
|
||||||
kind: TemplateResult,
|
|
||||||
state: State
|
|
||||||
) {
|
) {
|
||||||
return html`<div class=${classMap(classInfo)}>
|
return html`<div class=${classMap(classInfo)}>
|
||||||
<div class="affine-attachment-content">
|
<div class="affine-attachment-content">
|
||||||
@@ -251,8 +218,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
|||||||
${description}
|
${description}
|
||||||
</div>
|
</div>
|
||||||
${choose(state, [
|
${choose(state, [
|
||||||
['oversize', this.renderUpgradeButton],
|
['error', this.renderReloadButton],
|
||||||
['warning', this.renderReloadButton],
|
['error:oversize', this.renderUpgradeButton],
|
||||||
])}
|
])}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,11 +230,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
|||||||
|
|
||||||
protected renderWithVertical(
|
protected renderWithVertical(
|
||||||
classInfo: ClassInfo,
|
classInfo: ClassInfo,
|
||||||
icon: TemplateResult,
|
{ icon, title, description, state }: ResolvedStateInfo,
|
||||||
title: string,
|
kind: TemplateResult
|
||||||
description: string,
|
|
||||||
kind: TemplateResult,
|
|
||||||
state?: State
|
|
||||||
) {
|
) {
|
||||||
return html`<div class=${classMap(classInfo)}>
|
return html`<div class=${classMap(classInfo)}>
|
||||||
<div class="affine-attachment-content">
|
<div class="affine-attachment-content">
|
||||||
@@ -287,68 +251,40 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
|||||||
<div class="affine-attachment-banner">
|
<div class="affine-attachment-banner">
|
||||||
${kind}
|
${kind}
|
||||||
${choose(state, [
|
${choose(state, [
|
||||||
['oversize', this.renderUpgradeButton],
|
['error', this.renderReloadButton],
|
||||||
['warning', this.renderReloadButton],
|
['error:oversize', this.renderUpgradeButton],
|
||||||
])}
|
])}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderCard = () => {
|
protected renderCard = () => {
|
||||||
const { name, size, style } = this.model.props;
|
|
||||||
const cardStyle = style ?? AttachmentBlockStyles[1];
|
|
||||||
|
|
||||||
const theme = this.std.get(ThemeProvider).theme$.value;
|
const theme = this.std.get(ThemeProvider).theme$.value;
|
||||||
const loadingIcon = getLoadingIconWith(theme);
|
const loadingIcon = getLoadingIconWith(theme);
|
||||||
|
|
||||||
const blobState = this.blobState$.value;
|
const { name, size, style } = this.model.props;
|
||||||
const {
|
const cardStyle = style ?? AttachmentBlockStyles[1];
|
||||||
uploading = false,
|
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
|
||||||
downloading = false,
|
|
||||||
overSize = false,
|
const resolvedState = this.resourceController.resolveStateWith({
|
||||||
errorMessage,
|
loadingIcon,
|
||||||
} = blobState;
|
errorIcon: WarningIcon(),
|
||||||
const warning = !overSize && Boolean(errorMessage);
|
icon: AttachmentIcon(),
|
||||||
const error = overSize || warning;
|
title: name,
|
||||||
const state = this.determineState(downloading, uploading, overSize, error);
|
description: humanFileSize(size),
|
||||||
const loading = state === 'loading' || state === 'uploading';
|
});
|
||||||
|
|
||||||
const classInfo = {
|
const classInfo = {
|
||||||
'affine-attachment-card': true,
|
'affine-attachment-card': true,
|
||||||
[cardStyle]: true,
|
[cardStyle]: true,
|
||||||
error,
|
loading: resolvedState.loading,
|
||||||
loading,
|
error: resolvedState.error,
|
||||||
};
|
};
|
||||||
|
|
||||||
const icon = loading
|
|
||||||
? loadingIcon
|
|
||||||
: error
|
|
||||||
? WarningIcon()
|
|
||||||
: AttachmentIcon();
|
|
||||||
const title = uploading ? 'Uploading...' : loading ? 'Loading...' : name;
|
|
||||||
const description = errorMessage || humanFileSize(size);
|
|
||||||
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
|
|
||||||
|
|
||||||
return when(
|
return when(
|
||||||
cardStyle === 'cubeThick',
|
cardStyle === 'cubeThick',
|
||||||
() =>
|
() => this.renderWithVertical(classInfo, resolvedState, kind),
|
||||||
this.renderWithVertical(
|
() => this.renderWithHorizontal(classInfo, resolvedState, kind)
|
||||||
classInfo,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
kind,
|
|
||||||
state
|
|
||||||
),
|
|
||||||
() =>
|
|
||||||
this.renderWithHorizontal(
|
|
||||||
classInfo,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
kind,
|
|
||||||
state
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ export async function getAttachmentBlob(model: AttachmentBlockModel) {
|
|||||||
* the download process may take a long time!
|
* the download process may take a long time!
|
||||||
*/
|
*/
|
||||||
export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
||||||
const { host, model, blobUrl, blobState$ } = block;
|
const { host, model, blobUrl, resourceController } = block;
|
||||||
|
|
||||||
if (blobState$.peek().downloading) {
|
if (resourceController.state$.peek().downloading) {
|
||||||
toast(host, 'Download in progress...');
|
toast(host, 'Download in progress...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
block.updateBlobState({ downloading: true });
|
resourceController.updateState({ downloading: true });
|
||||||
|
|
||||||
toast(host, `Downloading ${shortName}`);
|
toast(host, `Downloading ${shortName}`);
|
||||||
|
|
||||||
@@ -67,34 +67,24 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
|||||||
tmpLink.dispatchEvent(event);
|
tmpLink.dispatchEvent(event);
|
||||||
tmpLink.remove();
|
tmpLink.remove();
|
||||||
|
|
||||||
block.updateBlobState({ downloading: false });
|
resourceController.updateState({ downloading: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshData(
|
export async function refreshData(block: AttachmentBlockComponent) {
|
||||||
std: BlockStdScope,
|
|
||||||
block: AttachmentBlockComponent
|
|
||||||
) {
|
|
||||||
const model = block.model;
|
const model = block.model;
|
||||||
const sourceId = model.props.sourceId$.peek();
|
const sourceId = model.props.sourceId$.peek();
|
||||||
if (!sourceId) return;
|
if (!sourceId) return;
|
||||||
|
|
||||||
const blobUrl = block.blobUrl;
|
|
||||||
if (blobUrl) {
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
block.blobUrl = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let blob = await std.store.blobSync.get(sourceId);
|
|
||||||
if (!blob) {
|
|
||||||
block.updateBlobState({ errorMessage: 'File not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = model.props.type$.peek();
|
const type = model.props.type$.peek();
|
||||||
|
|
||||||
blob = new Blob([blob], { type });
|
const url = await block.resourceController.createBlobUrlWith(type);
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
block.blobUrl = URL.createObjectURL(blob);
|
// Releases the previous url.
|
||||||
|
const prevUrl = block.blobUrl;
|
||||||
|
if (prevUrl) URL.revokeObjectURL(prevUrl);
|
||||||
|
|
||||||
|
block.blobUrl = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileType(file: File) {
|
export async function getFileType(file: File) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
{ "path": "../../widgets/slash-menu" },
|
{ "path": "../../widgets/slash-menu" },
|
||||||
{ "path": "../../../framework/global" },
|
{ "path": "../../../framework/global" },
|
||||||
{ "path": "../../../framework/std" },
|
{ "path": "../../../framework/std" },
|
||||||
{ "path": "../../../framework/store" },
|
{ "path": "../../../framework/store" }
|
||||||
{ "path": "../../../framework/sync" }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@blocksuite/icons": "^2.2.12",
|
"@blocksuite/icons": "^2.2.12",
|
||||||
"@blocksuite/std": "workspace:*",
|
"@blocksuite/std": "workspace:*",
|
||||||
"@blocksuite/store": "workspace:*",
|
"@blocksuite/store": "workspace:*",
|
||||||
|
"@blocksuite/sync": "workspace:*",
|
||||||
"@floating-ui/dom": "^1.6.13",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lottiefiles/dotlottie-wc": "^0.5.0",
|
"@lottiefiles/dotlottie-wc": "^0.5.0",
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
"./color-picker": "./src/color-picker/index.ts",
|
"./color-picker": "./src/color-picker/index.ts",
|
||||||
"./icons": "./src/icons/index.ts",
|
"./icons": "./src/icons/index.ts",
|
||||||
"./peek": "./src/peek/index.ts",
|
"./peek": "./src/peek/index.ts",
|
||||||
|
"./resource": "./src/resource/index.ts",
|
||||||
"./portal": "./src/portal/index.ts",
|
"./portal": "./src/portal/index.ts",
|
||||||
"./hover": "./src/hover/index.ts",
|
"./hover": "./src/hover/index.ts",
|
||||||
"./icon-button": "./src/icon-button/index.ts",
|
"./icon-button": "./src/icon-button/index.ts",
|
||||||
|
|||||||
153
blocksuite/affine/components/src/resource/index.ts
Normal file
153
blocksuite/affine/components/src/resource/index.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import type { BlobEngine, BlobState } from '@blocksuite/sync';
|
||||||
|
import { effect, type ReadonlySignal, signal } from '@preact/signals-core';
|
||||||
|
import type { TemplateResult } from 'lit-html';
|
||||||
|
|
||||||
|
export type ResourceKind = 'Blob' | 'File' | 'Image';
|
||||||
|
|
||||||
|
export type StateKind =
|
||||||
|
| 'loading'
|
||||||
|
| 'uploading'
|
||||||
|
| 'error'
|
||||||
|
| 'error:oversize'
|
||||||
|
| 'none';
|
||||||
|
|
||||||
|
export type StateInfo = {
|
||||||
|
icon: TemplateResult;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResolvedStateInfo = StateInfo & {
|
||||||
|
loading: boolean;
|
||||||
|
error: boolean;
|
||||||
|
state: StateKind;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ResourceController {
|
||||||
|
readonly state$ = signal<Partial<BlobState>>({});
|
||||||
|
|
||||||
|
private engine?: BlobEngine;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly blobId$: ReadonlySignal<string | undefined>,
|
||||||
|
readonly kind: ResourceKind = 'File'
|
||||||
|
) {}
|
||||||
|
|
||||||
|
setEngine(engine: BlobEngine) {
|
||||||
|
this.engine = engine;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
determineState(
|
||||||
|
hasExceeded: boolean,
|
||||||
|
hasError: boolean,
|
||||||
|
uploading: boolean,
|
||||||
|
downloading: boolean
|
||||||
|
): StateKind {
|
||||||
|
if (hasExceeded) return 'error:oversize';
|
||||||
|
if (hasError) return 'error';
|
||||||
|
if (uploading) return 'uploading';
|
||||||
|
if (downloading) return 'loading';
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveStateWith(
|
||||||
|
info: {
|
||||||
|
loadingIcon: TemplateResult;
|
||||||
|
errorIcon?: TemplateResult;
|
||||||
|
} & StateInfo
|
||||||
|
): ResolvedStateInfo {
|
||||||
|
const {
|
||||||
|
uploading = false,
|
||||||
|
downloading = false,
|
||||||
|
overSize = false,
|
||||||
|
errorMessage,
|
||||||
|
} = this.state$.value;
|
||||||
|
const hasExceeded = overSize;
|
||||||
|
const hasError = hasExceeded || Boolean(errorMessage);
|
||||||
|
const state = this.determineState(
|
||||||
|
hasExceeded,
|
||||||
|
hasError,
|
||||||
|
uploading,
|
||||||
|
downloading
|
||||||
|
);
|
||||||
|
const loading = state === 'uploading' || state === 'loading';
|
||||||
|
|
||||||
|
const { icon, title, description, loadingIcon, errorIcon } = info;
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
error: hasError,
|
||||||
|
loading,
|
||||||
|
state,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
result.icon = loadingIcon ?? icon;
|
||||||
|
result.title = state === 'uploading' ? 'Uploading...' : 'Loading...';
|
||||||
|
} else if (hasError) {
|
||||||
|
result.icon = errorIcon ?? icon;
|
||||||
|
result.description = errorMessage ?? description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState(state: Partial<BlobState>) {
|
||||||
|
this.state$.value = { ...this.state$.value, ...state };
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe() {
|
||||||
|
return effect(() => {
|
||||||
|
const blobId = this.blobId$.value;
|
||||||
|
if (!blobId) return;
|
||||||
|
|
||||||
|
const blobState$ = this.engine?.blobState$(blobId);
|
||||||
|
if (!blobState$) return;
|
||||||
|
|
||||||
|
const subscription = blobState$.subscribe(state => {
|
||||||
|
let { uploading, downloading } = state;
|
||||||
|
if (state.overSize || state.errorMessage) {
|
||||||
|
uploading = false;
|
||||||
|
downloading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateState({ ...state, uploading, downloading });
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async blob() {
|
||||||
|
const blobId = this.blobId$.peek();
|
||||||
|
if (!blobId) return null;
|
||||||
|
|
||||||
|
let blob: Blob | null = null;
|
||||||
|
let errorMessage: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
blob = (await this.engine?.get(blobId)) ?? null;
|
||||||
|
|
||||||
|
if (!blob) errorMessage = `${this.kind} not found`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
errorMessage = `Failed to retrieve ${this.kind}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage) this.updateState({ errorMessage });
|
||||||
|
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBlobUrlWith(type?: string) {
|
||||||
|
let blob = await this.blob();
|
||||||
|
if (!blob) return null;
|
||||||
|
|
||||||
|
if (type) blob = new Blob([blob], { type });
|
||||||
|
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
{ "path": "../shared" },
|
{ "path": "../shared" },
|
||||||
{ "path": "../../framework/global" },
|
{ "path": "../../framework/global" },
|
||||||
{ "path": "../../framework/std" },
|
{ "path": "../../framework/std" },
|
||||||
{ "path": "../../framework/store" }
|
{ "path": "../../framework/store" },
|
||||||
|
{ "path": "../../framework/sync" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ export const PackageList = [
|
|||||||
'blocksuite/framework/global',
|
'blocksuite/framework/global',
|
||||||
'blocksuite/framework/std',
|
'blocksuite/framework/std',
|
||||||
'blocksuite/framework/store',
|
'blocksuite/framework/store',
|
||||||
'blocksuite/framework/sync',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -437,6 +436,7 @@ export const PackageList = [
|
|||||||
'blocksuite/framework/global',
|
'blocksuite/framework/global',
|
||||||
'blocksuite/framework/std',
|
'blocksuite/framework/std',
|
||||||
'blocksuite/framework/store',
|
'blocksuite/framework/store',
|
||||||
|
'blocksuite/framework/sync',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2394,7 +2394,6 @@ __metadata:
|
|||||||
"@blocksuite/icons": "npm:^2.2.12"
|
"@blocksuite/icons": "npm:^2.2.12"
|
||||||
"@blocksuite/std": "workspace:*"
|
"@blocksuite/std": "workspace:*"
|
||||||
"@blocksuite/store": "workspace:*"
|
"@blocksuite/store": "workspace:*"
|
||||||
"@blocksuite/sync": "workspace:*"
|
|
||||||
"@floating-ui/dom": "npm:^1.6.13"
|
"@floating-ui/dom": "npm:^1.6.13"
|
||||||
"@lit/context": "npm:^1.1.2"
|
"@lit/context": "npm:^1.1.2"
|
||||||
"@preact/signals-core": "npm:^1.8.0"
|
"@preact/signals-core": "npm:^1.8.0"
|
||||||
@@ -2992,6 +2991,7 @@ __metadata:
|
|||||||
"@blocksuite/icons": "npm:^2.2.12"
|
"@blocksuite/icons": "npm:^2.2.12"
|
||||||
"@blocksuite/std": "workspace:*"
|
"@blocksuite/std": "workspace:*"
|
||||||
"@blocksuite/store": "workspace:*"
|
"@blocksuite/store": "workspace:*"
|
||||||
|
"@blocksuite/sync": "workspace:*"
|
||||||
"@floating-ui/dom": "npm:^1.6.13"
|
"@floating-ui/dom": "npm:^1.6.13"
|
||||||
"@lit/context": "npm:^1.1.2"
|
"@lit/context": "npm:^1.1.2"
|
||||||
"@lottiefiles/dotlottie-wc": "npm:^0.5.0"
|
"@lottiefiles/dotlottie-wc": "npm:^0.5.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user