From e2e00688a9f279f9853a7b32fe0aa9775567ffbe Mon Sep 17 00:00:00 2001 From: fundon Date: Fri, 23 May 2025 06:04:07 +0000 Subject: [PATCH] feat(core): add reload button to audio block (#12451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to: [BS-3143](https://linear.app/affine-design/issue/BS-3143/更新-loading-和错误样式) ## Summary by CodeRabbit - **New Features** - Added a reload button with an icon for audio blocks, allowing users to retry loading audio files if an error occurs. - Error messages are now displayed with actionable options when audio loading fails. - **Enhancements** - Audio file sizes are now shown in a human-readable format within the audio player. - Improved display of audio file information, including error messages and formatted descriptions. - **Style** - Updated styling for audio player and audio block components, including new styles for error states and reload button. - Renamed and refined audio player description styling for better layout and spacing. --- .../src/ui/audio-player/audio-player.css.ts | 3 +- .../ui/audio-player/audio-player.stories.tsx | 11 ++++-- .../src/ui/audio-player/audio-player.tsx | 9 ++--- .../audio/audio-block.css.ts | 19 ++++++++++ .../attachment-viewer/audio/audio-block.tsx | 35 +++++++++++++++---- .../root-app-sidebar/sidebar-audio-player.tsx | 1 - .../src/modules/media/entities/audio-media.ts | 18 ++++++---- 7 files changed, 72 insertions(+), 24 deletions(-) diff --git a/packages/frontend/component/src/ui/audio-player/audio-player.css.ts b/packages/frontend/component/src/ui/audio-player/audio-player.css.ts index 32fb524936..ebb4dd05fe 100644 --- a/packages/frontend/component/src/ui/audio-player/audio-player.css.ts +++ b/packages/frontend/component/src/ui/audio-player/audio-player.css.ts @@ -60,8 +60,9 @@ export const spacer = style({ flex: 1, }); -export const sizeInfo = style({ +export const description = style({ display: 'flex', + gap: '8px', alignItems: 'center', fontSize: cssVar('fontXs'), color: cssVarV2('text/secondary'), diff --git a/packages/frontend/component/src/ui/audio-player/audio-player.stories.tsx b/packages/frontend/component/src/ui/audio-player/audio-player.stories.tsx index c58288bba5..de73d5b6a5 100644 --- a/packages/frontend/component/src/ui/audio-player/audio-player.stories.tsx +++ b/packages/frontend/component/src/ui/audio-player/audio-player.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import bytes from 'bytes'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AudioPlayer, MiniAudioPlayer } from './audio-player'; @@ -159,6 +160,10 @@ const AudioWrapper = () => { } }, []); + const description = useMemo(() => { + return audioFile ? <>{bytes(audioFile.size)} : null; + }, [audioFile]); + useEffect(() => { const audio = audioRef.current; if (!audio || !audioFile) return; @@ -296,7 +301,7 @@ const AudioWrapper = () => { /> { /> { export interface AudioPlayerProps { // Audio metadata name: string; - size: number | ReactNode; // the size entry may be used for drawing error message + description?: ReactNode; // Display file size or error message waveform: number[] | null; // Playback state playbackState: 'idle' | 'playing' | 'paused' | 'stopped'; @@ -52,7 +51,7 @@ const playbackRates = [0.5, 0.75, 1, 1.5, 1.75, 2, 3]; export const AudioPlayer = ({ name, - size, + description, playbackState, seekTime, duration, @@ -108,9 +107,7 @@ export const AudioPlayer = ({
{name}
-
- {typeof size === 'number' ? bytes(size) : size} -
+
{description}
diff --git a/packages/frontend/core/src/blocksuite/attachment-viewer/audio/audio-block.css.ts b/packages/frontend/core/src/blocksuite/attachment-viewer/audio/audio-block.css.ts index 81c0fc4cea..132cc0fb37 100644 --- a/packages/frontend/core/src/blocksuite/attachment-viewer/audio/audio-block.css.ts +++ b/packages/frontend/core/src/blocksuite/attachment-viewer/audio/audio-block.css.ts @@ -28,5 +28,24 @@ export const notesButtonIcon = style({ }); export const error = style({ + display: 'flex', color: cssVarV2('aI/errorText'), }); + +export const reloadButton = style({ + display: 'flex', + alignItems: 'center', + padding: '0 4px', + gap: '4px', + border: 'none', + background: 'none', + cursor: 'pointer', + outline: 'none', + color: cssVarV2('button/primary'), + fontSize: cssVar('fontSm'), + fontWeight: 500, +}); + +export const reloadButtonIcon = style({ + fontSize: 16, +}); diff --git a/packages/frontend/core/src/blocksuite/attachment-viewer/audio/audio-block.tsx b/packages/frontend/core/src/blocksuite/attachment-viewer/audio/audio-block.tsx index f47ec4ec9e..a3a8e16ee0 100644 --- a/packages/frontend/core/src/blocksuite/attachment-viewer/audio/audio-block.tsx +++ b/packages/frontend/core/src/blocksuite/attachment-viewer/audio/audio-block.tsx @@ -16,7 +16,9 @@ import { AudioAttachmentService } from '@affine/core/modules/media/services/audi import { Trans, useI18n } from '@affine/i18n'; import track from '@affine/track'; import type { AttachmentBlockModel } from '@blocksuite/affine/model'; +import { ResetIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; +import bytes from 'bytes'; import { useCallback, useEffect, useMemo, useState } from 'react'; import type { AttachmentViewerProps } from '../types'; @@ -31,6 +33,8 @@ const AttachmentAudioPlayer = ({ block }: { block: AudioAttachmentBlock }) => { const [preflightChecking, setPreflightChecking] = useState(false); const transcribing = useLiveData(block.transcriptionJob.transcribing$) || preflightChecking; + const loading = useLiveData(audioMedia.loading$); + const loadingError = useLiveData(audioMedia.loadError$); const error = useLiveData(block.transcriptionJob.error$); const transcribed = useLiveData(block.hasTranscription$); const handleClick = useCallback((e: React.MouseEvent) => { @@ -65,6 +69,10 @@ const AttachmentAudioPlayer = ({ block }: { block: AudioAttachmentBlock }) => { [audioMedia] ); + const reload = useCallback(() => { + audioMedia?.revalidateBuffer(); + }, [audioMedia]); + const t = useI18n(); const enableAi = useEnableAI(); @@ -179,17 +187,30 @@ const AttachmentAudioPlayer = ({ block }: { block: AudioAttachmentBlock }) => { return inner; }, [enableAi, transcribing, handleNotesClick, t]); - const sizeEntry = useMemo(() => { - if (error) { + const descriptionEntry = useMemo(() => { + if (loadingError) { + return ( + <> +
{loadingError.message}
+ + + ); + } + + if (!loading && error) { return
{error.message}
; } - return block.props.props.size; - }, [error, block.props.props.size]); + + return <>{bytes(block.props.props.size)}; + }, [loading, loadingError, error, reload, block.props.props.size]); return ( { return audioAttachmentBlock; }; -export const AudioBlockEmbedded = (props: AttachmentViewerProps) => { - const audioAttachmentBlock = useAttachmentMediaBlock(props.model); +export const AudioBlockEmbedded = ({ model }: AttachmentViewerProps) => { + const audioAttachmentBlock = useAttachmentMediaBlock(model); const transcriptionBlock = useLiveData( audioAttachmentBlock?.transcriptionBlock$ ); diff --git a/packages/frontend/core/src/components/root-app-sidebar/sidebar-audio-player.tsx b/packages/frontend/core/src/components/root-app-sidebar/sidebar-audio-player.tsx index 4dcee443c3..d64779aa70 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/sidebar-audio-player.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/sidebar-audio-player.tsx @@ -130,7 +130,6 @@ export const SidebarAudioPlayer = () => { { return { waveform, duration }; }); + // `MediaSession` is available + private readonly available = 'mediaSession' in navigator; + private readonly audioElement: HTMLAudioElement; private updatePlaybackState( @@ -194,7 +197,10 @@ export class AudioMedia extends Entity { `Calculate audio stats time: ${performance.now() - startTime}ms` ); }), - onStart(() => this.loading$.setValue(true)), + onStart(() => { + this.loadError$.setValue(null); + this.loading$.setValue(true); + }), onComplete(() => { this.loading$.setValue(false); }), @@ -212,7 +218,7 @@ export class AudioMedia extends Entity { } private setupMediaSession() { - if (!('mediaSession' in navigator)) { + if (!this.available) { return; } @@ -237,14 +243,14 @@ export class AudioMedia extends Entity { } private updateMediaSessionMetadata() { - if (!('mediaSession' in navigator) || !this.props.metadata) { + if (!this.available || !this.props.metadata) { return; } navigator.mediaSession.metadata = this.props.metadata; } private updateMediaSessionPositionState(seekTime: number) { - if (!('mediaSession' in navigator)) { + if (!this.available) { return; } @@ -260,7 +266,7 @@ export class AudioMedia extends Entity { } private updateMediaSessionPlaybackState(state: AudioMediaPlaybackState) { - if (!('mediaSession' in navigator)) { + if (!this.available) { return; } @@ -270,7 +276,7 @@ export class AudioMedia extends Entity { } private cleanupMediaSession() { - if (!('mediaSession' in navigator)) { + if (!this.available) { return; } navigator.mediaSession.metadata = null;