feat: basic ui layout

This commit is contained in:
鲁树人
2023-05-07 23:29:37 +01:00
parent 53682a1cdb
commit 38aa81b5bc
16 changed files with 375 additions and 104 deletions

View File

@@ -1 +0,0 @@
/* empty file here */

View File

@@ -1,11 +1,22 @@
import './App.css';
import { Box, Center, Container } from '@chakra-ui/react';
import { SelectFile } from './SelectFile';
import { FileListing } from './features/file-listing/FileListing';
import { Footer } from './Footer';
function App() {
return (
<main>
<SelectFile />
</main>
<Box height="full" width="full" pt="4">
<Container maxW="container.large">
<Center>
<SelectFile />
</Center>
<Box mt="8">
<FileListing />
</Box>
<Footer />
</Container>
</Box>
);
}

33
src/Footer.tsx Normal file
View File

@@ -0,0 +1,33 @@
import { Center, Link, Text } from '@chakra-ui/react';
export function Footer() {
return (
<Center height="footer.container">
<Center
height="footer.content"
fontSize="sm"
textAlign="center"
position="fixed"
bottom="0"
w="full"
bg="gray.100"
color="gray.800"
left="0"
flexDir="column"
>
<Text> (x.x.x) - </Text>
<Text>
Copyright © 2019 - 2023{' '}
<Link href="https://git.unlock-music.dev/um" isExternal>
UnlockMusic
</Link>{' '}
|
<Link href="https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE" isExternal>
MIT许可协议
</Link>
</Text>
</Center>
</Center>
);
}

View File

@@ -1,5 +0,0 @@
import styled from '@emotion/styled';
export const PointerLabel = styled.label`
cursor: pointer;
`;

View File

@@ -1,25 +1,44 @@
import { Box, Stack, Text } from '@chakra-ui/react';
import { UnlockIcon } from '@chakra-ui/icons';
import { useId } from 'react';
import { PointerLabel } from './PointerLabel';
import { Box, Text } from '@chakra-ui/react';
import { UnlockIcon } from '@chakra-ui/icons';
export function SelectFile() {
const id = useId();
return (
<Box borderWidth="1px" borderRadius="lg" p="6">
<Stack alignItems="center">
<UnlockIcon />
<Box>
<PointerLabel htmlFor={id}>
<Text as="span" color="teal.400">
</Text>
</PointerLabel>
<input id={id} type="file" hidden multiple />
</Box>
</Stack>
<Box
as="label"
htmlFor={id}
w="100%"
maxW={480}
borderWidth="1px"
borderRadius="lg"
transitionDuration="0.5s"
p="6"
cursor="pointer"
display="flex"
flexDir="column"
alignItems="center"
_hover={{
borderColor: 'gray.400',
bg: 'gray.50',
}}
>
<Box pb={3}>
<UnlockIcon boxSize={8} />
</Box>
<Box textAlign="center">
{/* 将文件拖到此处,或 */}
<Text as="span" color="teal.400">
</Text>
<input id={id} type="file" hidden multiple />
<Text fontSize="sm" opacity="50%">
</Text>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,56 @@
import { useEffect } from 'react';
import { Avatar, Box, Table, TableContainer, Tbody, Td, Text, Th, Thead, Tr, Wrap, WrapItem } from '@chakra-ui/react';
import { addNewFile, selectFiles } from './fileListingSlice';
import { useAppDispatch, useAppSelector } from '../../hooks';
export function FileListing() {
const dispatch = useAppDispatch();
const files = useAppSelector(selectFiles);
useEffect(() => {
// FIXME: Remove test data
if (files.length === 0) {
dispatch(addNewFile({ id: String(Date.now()), fileName: '测试文件名.mgg', blobURI: '' }));
}
}, []);
return (
<TableContainer>
<Table variant="striped">
<Thead>
<Tr>
<Th w="1%"></Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{files.map((file) => (
<Tr key={file.id}>
<Td>
{file.metadata.cover && <Avatar size="sm" name="专辑封面" src={file.metadata.cover} />}
{!file.metadata.cover && <Text></Text>}
</Td>
<Td>
<Box as="h4" fontWeight="semibold" mt="1">
{file.metadata.name || file.fileName}
</Box>
<Text>: {file.metadata.album}</Text>
<Text>: {file.metadata.artist}</Text>
<Text>: {file.metadata.albumArtist}</Text>
</Td>
<Td>
<Wrap>
<WrapItem></WrapItem>
<WrapItem></WrapItem>
<WrapItem></WrapItem>
</Wrap>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
);
}

View File

@@ -0,0 +1,79 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../../store';
export enum ProcessState {
UNTOUCHED = 'UNTOUCHED',
COMPLETE = 'COMPLETE',
ERROR = 'ERROR',
}
export enum ListingMode {
LIST = 'LIST',
CARD = 'CARD',
}
export interface AudioMetadata {
name: string;
artist: string;
album: string;
albumArtist: string;
cover: string; // blob uri
}
export interface DecryptedAudioFile {
id: string;
fileName: string;
raw: string; // blob uri
decrypted: string; // blob uri
state: ProcessState;
errorMessage: null | string;
metadata: AudioMetadata;
}
export interface FileListingState {
files: DecryptedAudioFile[];
displayMode: ListingMode;
}
const initialState: FileListingState = {
files: [],
displayMode: ListingMode.LIST,
};
export const fileListingSlice = createSlice({
name: 'fileListing',
initialState,
reducers: {
addNewFile: (state, { payload }: PayloadAction<{ id: string; fileName: string; blobURI: string }>) => {
state.files.push({
id: payload.id,
fileName: payload.fileName,
raw: payload.blobURI,
decrypted: '',
state: ProcessState.UNTOUCHED,
errorMessage: null,
metadata: {
name: '',
artist: '',
album: '',
albumArtist: '',
cover: '',
},
});
},
setDecryptedContent: (state, { payload }: PayloadAction<{ id: string; decryptedBlobURI: string }>) => {
const file = state.files.find((file) => file.id === payload.id);
if (file) {
file.decrypted = payload.decryptedBlobURI;
}
},
},
});
export const { addNewFile, setDecryptedContent } = fileListingSlice.actions;
export const selectFileCount = (state: RootState) => state.fileListing.files.length;
export const selectFiles = (state: RootState) => state.fileListing.files;
export const selectFileListingMode = (state: RootState) => state.fileListing.displayMode;
export default fileListingSlice.reducer;

6
src/hooks.ts Normal file
View File

@@ -0,0 +1,6 @@
import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@@ -1,69 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -1,14 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import App from './App';
import { ChakraProvider } from '@chakra-ui/react';
import { Provider } from 'react-redux';
import { store } from './store';
import { theme } from './theme';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<ChakraProvider>
<App />
<ChakraProvider theme={theme}>
<Provider store={store}>
<App />
</Provider>
</ChakraProvider>
</React.StrictMode>
);

11
src/store.ts Normal file
View File

@@ -0,0 +1,11 @@
import { configureStore } from '@reduxjs/toolkit';
import fileListing from './features/file-listing/fileListingSlice';
export const store = configureStore({
reducer: {
fileListing: fileListing,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

17
src/theme.ts Normal file
View File

@@ -0,0 +1,17 @@
import { extendTheme } from '@chakra-ui/react';
export const theme = extendTheme({
styles: {
global: {
body: {
minHeight: '100vh',
},
},
},
sizes: {
footer: {
container: '7rem',
content: '5rem',
},
},
});