mirror of
https://git.unlock-music.dev/um/um-react.git
synced 2025-07-07 06:52:07 +08:00
Merge remote-tracking branch 'origin/main' into feat/file-row
# Conflicts: # src/features/file-listing/FileRow.tsx
This commit is contained in:
@ -18,6 +18,7 @@ import {
|
||||
import { DecryptedAudioFile, deleteFile, ProcessState } from './fileListingSlice';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useAppDispatch } from '~/hooks';
|
||||
import coverFallback from '~/assets/no-cover.svg';
|
||||
|
||||
interface FileRowProps {
|
||||
id: string;
|
||||
@ -28,6 +29,7 @@ export function FileRow({ id, file }: FileRowProps) {
|
||||
const { isOpen, onClose } = useDisclosure({ defaultIsOpen: true });
|
||||
const dispatch = useAppDispatch();
|
||||
const isDecrypted = file.state === ProcessState.COMPLETE;
|
||||
const metadata = file.metadata;
|
||||
|
||||
const nameWithoutExt = file.fileName.replace(/\.[a-z\d]{3,6}$/, '');
|
||||
const decryptedName = nameWithoutExt + '.' + file.ext;
|
||||
@ -55,7 +57,7 @@ export function FileRow({ id, file }: FileRowProps) {
|
||||
|
||||
return (
|
||||
<Collapse in={isOpen} animateOpacity unmountOnExit startingHeight={0} style={{ width: '100%' }}>
|
||||
<Card w="full">
|
||||
<Card w="full" data-testid="file-row">
|
||||
<CardBody>
|
||||
<Grid
|
||||
templateAreas={{
|
||||
@ -82,26 +84,33 @@ export function FileRow({ id, file }: FileRowProps) {
|
||||
>
|
||||
<GridItem area="cover">
|
||||
<Center w="160px" h="160px" m="auto">
|
||||
<Image
|
||||
boxSize='160px'
|
||||
objectFit='cover'
|
||||
src={file.metadata.cover}
|
||||
alt={file.metadata.album}
|
||||
fallbackSrc='https://via.placeholder.com/160'
|
||||
/>
|
||||
{metadata && (
|
||||
<Image
|
||||
objectFit="cover"
|
||||
src={metadata.cover}
|
||||
alt={`"${metadata.album}" 的专辑封面`}
|
||||
fallbackSrc={coverFallback}
|
||||
/>
|
||||
)}
|
||||
</Center>
|
||||
</GridItem>
|
||||
<GridItem area="title">
|
||||
<Box w="full" as="h4" fontWeight="semibold" mt="1" textAlign={{ base: 'center', md: 'left' }}>
|
||||
{file.metadata.name || nameWithoutExt}
|
||||
<span data-testid="audio-meta-song-name">{metadata?.name ?? nameWithoutExt}</span>
|
||||
</Box>
|
||||
</GridItem>
|
||||
<GridItem area="meta">
|
||||
{isDecrypted && (
|
||||
{isDecrypted && metadata && (
|
||||
<Box>
|
||||
<Text>专辑: {file.metadata.album}</Text>
|
||||
<Text>艺术家: {file.metadata.artist}</Text>
|
||||
<Text>专辑艺术家: {file.metadata.albumArtist}</Text>
|
||||
<Text>
|
||||
专辑: <span data-testid="audio-meta-album-name">{metadata.album}</span>
|
||||
</Text>
|
||||
<Text>
|
||||
艺术家: <span data-testid="audio-meta-song-artist">{metadata.artist}</span>
|
||||
</Text>
|
||||
<Text>
|
||||
专辑艺术家: <span data-testid="audio-meta-album-artist">{metadata.albumArtist}</span>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</GridItem>
|
||||
|
18
src/features/file-listing/__tests__/FileListing.test.tsx
Normal file
18
src/features/file-listing/__tests__/FileListing.test.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { FileListing } from '../FileListing';
|
||||
import { renderWithProviders, screen } from '~/test-utils/test-helper';
|
||||
import { ListingMode } from '../fileListingSlice';
|
||||
import { dummyFiles } from './__fixture__/file-list';
|
||||
|
||||
test('should be able to render a list of 3 items', () => {
|
||||
renderWithProviders(<FileListing />, {
|
||||
preloadedState: {
|
||||
fileListing: {
|
||||
displayMode: ListingMode.LIST,
|
||||
files: dummyFiles,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getAllByTestId('file-row')).toHaveLength(3);
|
||||
expect(screen.getByText('Für Alice')).toBeInTheDocument();
|
||||
});
|
24
src/features/file-listing/__tests__/FileRow.test.tsx
Normal file
24
src/features/file-listing/__tests__/FileRow.test.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { renderWithProviders, screen } from '~/test-utils/test-helper';
|
||||
import { untouchedFile } from './__fixture__/file-list';
|
||||
import { FileRow } from '../FileRow';
|
||||
import { completedFile } from './__fixture__/file-list';
|
||||
|
||||
test('should render no metadata when unavailable', () => {
|
||||
renderWithProviders(<FileRow id="file://ready" file={untouchedFile} />);
|
||||
|
||||
expect(screen.getAllByTestId('file-row')).toHaveLength(1);
|
||||
expect(screen.getByTestId('audio-meta-song-name')).toHaveTextContent('ready');
|
||||
expect(screen.queryByTestId('audio-meta-album-name')).toBeFalsy();
|
||||
expect(screen.queryByTestId('audio-meta-song-artist')).toBeFalsy();
|
||||
expect(screen.queryByTestId('audio-meta-album-artist')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should render metadata when file has been processed', () => {
|
||||
renderWithProviders(<FileRow id="file://done" file={completedFile} />);
|
||||
|
||||
expect(screen.getAllByTestId('file-row')).toHaveLength(1);
|
||||
expect(screen.getByTestId('audio-meta-song-name')).toHaveTextContent('Für Alice');
|
||||
expect(screen.getByTestId('audio-meta-album-name')).toHaveTextContent("NOW That's What I Call Cryptography 2023");
|
||||
expect(screen.getByTestId('audio-meta-song-artist')).toHaveTextContent('Jixun');
|
||||
expect(screen.getByTestId('audio-meta-album-artist')).toHaveTextContent('Cipher Lovers');
|
||||
});
|
43
src/features/file-listing/__tests__/__fixture__/file-list.ts
Normal file
43
src/features/file-listing/__tests__/__fixture__/file-list.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { DecryptedAudioFile, ProcessState } from '../../fileListingSlice';
|
||||
|
||||
export const untouchedFile: DecryptedAudioFile = {
|
||||
fileName: 'ready.bin',
|
||||
raw: 'blob://localhost/file-a',
|
||||
decrypted: '',
|
||||
ext: '',
|
||||
state: ProcessState.UNTOUCHED,
|
||||
errorMessage: null,
|
||||
metadata: null,
|
||||
};
|
||||
|
||||
export const completedFile: DecryptedAudioFile = {
|
||||
fileName: 'hello-b.bin',
|
||||
raw: 'blob://localhost/file-b',
|
||||
decrypted: 'blob://localhost/file-b-decrypted',
|
||||
ext: 'flac',
|
||||
state: ProcessState.COMPLETE,
|
||||
errorMessage: null,
|
||||
metadata: {
|
||||
name: 'Für Alice',
|
||||
artist: 'Jixun',
|
||||
albumArtist: 'Cipher Lovers',
|
||||
album: "NOW That's What I Call Cryptography 2023",
|
||||
cover: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const fileWithError: DecryptedAudioFile = {
|
||||
fileName: 'hello-c.bin',
|
||||
raw: 'blob://localhost/file-c',
|
||||
decrypted: 'blob://localhost/file-c-decrypted',
|
||||
ext: 'flac',
|
||||
state: ProcessState.ERROR,
|
||||
errorMessage: 'Could not decrypt blah blah',
|
||||
metadata: null,
|
||||
};
|
||||
|
||||
export const dummyFiles: Record<string, DecryptedAudioFile> = {
|
||||
'file://untouched': untouchedFile,
|
||||
'file://completed': completedFile,
|
||||
'file://error': fileWithError,
|
||||
};
|
@ -31,7 +31,7 @@ export interface DecryptedAudioFile {
|
||||
decrypted: string; // blob uri
|
||||
state: ProcessState;
|
||||
errorMessage: null | string;
|
||||
metadata: AudioMetadata;
|
||||
metadata: null | AudioMetadata;
|
||||
}
|
||||
|
||||
export interface FileListingState {
|
||||
@ -69,13 +69,7 @@ export const fileListingSlice = createSlice({
|
||||
ext: '',
|
||||
state: ProcessState.UNTOUCHED,
|
||||
errorMessage: null,
|
||||
metadata: {
|
||||
name: '',
|
||||
artist: '',
|
||||
album: '',
|
||||
albumArtist: '',
|
||||
cover: '',
|
||||
},
|
||||
metadata: null,
|
||||
};
|
||||
},
|
||||
setDecryptedContent: (state, { payload }: PayloadAction<{ id: string; decryptedBlobURI: string }>) => {
|
||||
|
Reference in New Issue
Block a user