Merge remote-tracking branch 'origin/main' into feat/file-row

# Conflicts:
#	src/features/file-listing/FileRow.tsx
This commit is contained in:
HouKunLin
2023-05-17 09:18:41 +08:00
22 changed files with 702 additions and 85 deletions

View File

@ -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>

View 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();
});

View 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');
});

View 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,
};

View File

@ -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 }>) => {